类与对象
C++面向对象的三大特性:封装、继承、多态
C++中,任何事物都为对象 ,对象有其属性和行为
具有相同性质 的对象,我们可以抽象称为类
封装
封装的实现
意义:
将属性和行为作为一个整体,表现生活中的事物
将属性和行为加以权限控制
语法:class <类名>{ <访问权限>: <属性/行为>}
一些名词:
成员:类中的属性和行为的统称
成员属性(成员变量):类中的属性
成员函数(成员方法):类中的函数
e.g.1 设计一个圆类,求其周长
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream>
using namespace std ;
// 设计一个圆类,求其周长
const double PI = 3.14 ;
class circle {
// 访问权限: 公共权限
public:
// 属性:半径
int r;
// 行为:获取周长
double calC (){
return 2 * PI * r;
}
};
int main (){
// 通过类创建一个对象——实例化
circle c1;
// 给对象的属性进行赋值,使用.
c1.r = 10 ;
// 调用行为
cout << "r=" << c1. calC () << endl;
return 0 ;
}
e.g.2 设计一个学生类,属性有姓名和学号,可以赋值并显示姓名和学号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream>
#include <string>
using namespace std ;
// 设计一个学生类,有姓名和学号,可以赋值并显示
class student {
public:
//属性
string name; // 姓名
int id; // 学号
// 行为
void display (){
cout << "name:" << name << endl;
cout << "id:" << id << endl;
}
void set_info ( string s_name , int s_id ){
name = s_name;
id = s_id;
}
};
int main (){
student stu1; // 实例化
student stu2;
// 一般赋值
stu1.name = "Jack" ;
stu1.id = 1 ;
stu1. display ();
// 通过行为赋值
stu2. set_info ( "Mary" , 2 );
stu2. display ();
return 0 ;
}
权限
类可以将属性和方法放在不同权限下:
public:公共权限,成员类内可以访问,类外也可以访问
protected:保护权限,成员类内可以访问,类外不可以访问,子类可以访问父类的保护成员
private:私有权限,成员类内可以访问,类外不可以访问,
子类不可访问父类的私有成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <iostream>
#include <string>
using namespace std ;
class person {
public: //公共权限
string name;
protected: // 保护权限
string car;
private: // 私有权限
int password;
public:
// 类内可以访问
void f (){
name = "Jack" ;
car = "xiaomisu7" ;
password = 123456 ;
}
};
int main (){
person p;
p.name = "Mary" ;
//p.car = "?" // protected 类外不可访问
//p.password = 123 // private 类外不可访问
return 0 ;
}
struct与class的区别
C++中class与struct的唯一区别在于默认的访问权限不同
成员属性私有化
优点:
基本思路:用公有方法对私有属性进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <iostream>
#include <string>
using namespace std ;
class person {
// 属性私有
private:
string name; // 姓名:可读可写
int age = 18 ; // 年龄:只读
string hobby; // 爱好:只写
// 方法公有
public:
// 设置姓名
void setName ( string s ){
name = s;
}
// 获取姓名
void getName (){
cout << "name:" << name << endl;
}
// 获取年龄
void getAge (){
cout << "age:" << age << endl;
}
// 设置爱好
void setHobby ( string s ){
hobby = s;
}
};
int main (){
person p;
p. setName ( "Jack" );
p. getAge ();
p. setHobby ( "jerk" );
return 0 ;
}
对象的初始化和清理
对象的初始化和清理是很重要的安全问题:
一个对象或变量没有初始状态,对其使用后果未知
使用完一个对象或变量,没有及时清理,也会造成安全问题
构造函数与析构函数
C++提供了这两种函数,被编译器自动调用,完成对象初始化与清理工作
这两个工作是强制执行的,若我们不提供,编译器会提供自己的构造函数与析构函数,他们是空实现
构造函数: 用于创建对象时为对象成员赋值
语法:<类名>(){}
无返回值,不用写void
函数名与类名相同
构造函数可以有参数,可以发生重载
调用对象时会自动调用构造,只会调用一次
析构函数:用于对象销毁前自动调用,执行清理操作
语法:~<类名>(){}
无返回值,不用写void
函数名与类名相同,名前面加~
析构函数不可以有参数,不可发生重载
程序在对象销毁前会自动调用析构。只会调用一次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream>
using namespace std ;
class person {
public:
// 构造函数
person (){
cout << "构造函数被调用" << endl;
}
// 析构函数
~person (){
cout << "析构函数被调用" << endl;
}
};
int main (){
person p; // 栈上的数据,执行完后会被释放,故执行析构函数
return 0 ;
}
构造函数的分类与调用
分类:
参数分:有参构造与无参构造(默认构造)
类型分:普通构造与拷贝构造
调用:
注意:
无参构造时不需要加括号,否则编译器会认为是一个函数声明
显式声明中,单纯调用对象<类名>([值])称为匿名对象 ,特点是当前行执行后就会被立即回收
不要利用拷贝构造函数初始化匿名对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <iostream>
using namespace std ;
class person {
public:
// 无参构造(默认构造)
person (){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person ( int a ){
age = a;
cout << "有参构造函数被调用" << endl;
}
// 拷贝构造
person ( const person & p ){
age = p.age; // 将传入数据拷贝到当前对象
cout << "拷贝构造函数被调用" << endl;
}
// 析构函数
~person (){
cout << "析构函数被调用" << endl;
}
int age;
};
int main (){
// 括号法
person p; // 默认构造函数被调用(不加括号)
person p1 ( 10 ); // 有参构造函数被调用
person p2 (p1); // 拷贝构造函数被调用
cout << "age of p1:" << p1.age << endl;
cout << "age of p2:" << p2.age << endl;
// 显式法
person p3 = person ( 20 );
person p4 = person (p3);
// 隐式转换法
person p5 = 10 ; // 相当于 person p5 = person(10);
person p6 = p5; // 相当于 person p6 = person(p5);
return 0 ;
}
拷贝构造函数调用时机
拷贝构造调用的三种情况:
使用一个已经创建完毕的对象来初始化一个新对象
值传递的方式给函数参数传值
以值方式返回局部对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <iostream>
using namespace std ;
class person {
public:
// 无参构造(默认构造)
person (){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person ( int a ){
age = a;
cout << "有参构造函数被调用" << endl;
}
// 拷贝构造
person ( const person & p ){
age = p.age; // 将传入数据拷贝到当前对象
cout << "拷贝构造函数被调用" << endl;
}
// 析构函数
~person (){
cout << "析构函数被调用" << endl;
}
int age;
};
// 使用已经创建完毕的对象来初始化一个新对象
void test1 (){
person p1 ( 20 );
person p2 (p1);
}
// 值传递方式给函数参数传值
void test2 ( person p ){
return ;
}
// 值传递返回局部对象
person test3 (){
person p1;
return p1;
}
int main (){
person p1;
test1 ();
test2 (p1);
person p2 = test3 ();
return 0 ;
}
无参构造函数被调用
----------------
有参构造函数被调用
拷贝构造函数被调用
析构函数被调用
析构函数被调用
----------------
拷贝构造函数被调用
析构函数被调用
----------------
无参构造函数被调用
析构函数被调用
析构函数被调用
构造函数调用规则
默认下,编译器至少给一个类添加三个函数:
默认构造函数(无参,函数体为空)
默认析构函数(无参,函数体为空)
默认拷贝构造函数,对属性值拷贝
调用规则:
若用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数 ,C++不再提供其他构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 #include <iostream>
using namespace std ;
class person {
public:
// 无参构造(默认构造)
person (){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person ( int a ){
age = a;
cout << "有参构造函数被调用" << endl;
}
// // 拷贝构造
// person(const person &p){
// age = p.age; // 将传入数据拷贝到当前对象
// cout << "拷贝构造函数被调用" << endl;
// }
// 析构函数
~person (){
cout << "析构函数被调用" << endl;
}
int age;
};
int main (){
person p;
p.age = 18 ;
person p2 (p);
cout << "p2的年龄是:" << p2.age << endl;
return 0 ;
}
```
此种情况下,编译器使用了默认拷贝构造,进行了值传递
```c ++
#include <iostream>
using namespace std ;
class person {
public:
// // 无参构造(默认构造)
// person(){
// cout << "无参构造函数被调用" << endl;
// }
// 有参构造
person ( int a ){
age = a;
cout << "有参构造函数被调用" << endl;
}
// 析构函数
~person (){
cout << "析构函数被调用" << endl;
}
int age;
};
int main (){
person p;
p.age = 18 ;
person p2 (p);
cout << "p2的年龄是:" << p2.age << endl;
return 0 ;
}
```
此种情况下,由于有有参函数,编译器不会提供默认无参函数,同时会报错没有默认无参函数
```c ++
#include <iostream>
using namespace std ;
class person {
public:
// // 无参构造(默认构造)
// person(){
// cout << "无参构造函数被调用" << endl;
// }
// // 有参构造
// person(int a){
// age = a;
// cout << "有参构造函数被调用" << endl;
// }
// 拷贝构造
person ( const person & p ){
age = p.age; // 将传入数据拷贝到当前对象
cout << "拷贝构造函数被调用" << endl;
}
// 析构函数
~person (){
cout << "析构函数被调用" << endl;
}
int age;
};
int main (){
person p;
p.age = 18 ;
person p2 (p);
cout << "p2的年龄是:" << p2.age << endl;
return 0 ;
}
此种情况下,提供了拷贝函数,则编译器不再提供任何构造函数,同时报错没有默认构造函数
深拷贝与浅拷贝
浅拷贝:简单的赋值操作(默认拷贝构造即为浅拷贝 )
深拷贝:在堆区重新申请空间,进行拷贝操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <iostream>
using namespace std ;
class person {
public:
// 无参构造(默认构造)
person (){
cout << "无参构造函数被调用" << endl;
}
// 有参构造
person ( int a , int iheight ){
age = a;
height = new int (iheight);
cout << "有参构造函数被调用" << endl;
}
// // 拷贝构造
// person(const person &p){
// age = p.age; // 将传入数据拷贝到当前对象
// cout << "拷贝构造函数被调用" << endl;
// }
// 析构函数
~person (){
// 在析构函数中进行堆区数据释放
if (height != NULL ){
delete height;
height = NULL ;
}
cout << "析构函数被调用" << endl;
}
int age;
int* height; // 身高,使用指针将数据创建在堆区
};
int main (){
person p1 ( 18 , 160 );
cout << "p1的年龄是:" << p1.age << endl;
cout << "p1的身高是:" << * p1.height << endl;
person p2 (p1);
cout << "p2的年龄是:" << p2.age << endl;
cout << "p2的身高是:" << * p2.height << endl;
return 0 ;
}
该代码会卡住,原因是利用拷贝构造只进行了值传递,p1与p2执行完毕后,均执行析构函数,对height指向的空间进行释放,但是其中一个已经执行释放后,另一个再次释放会产生非法操作,故出错
这也就是浅拷贝的缺点:只进行了值传递,我们需要利用深拷贝来结局
// 拷贝构造
person ( const person & p){
age = p.age; // 将传入数据拷贝到当前对象
// 深拷贝,在堆区开辟新的空间
height = new int ( * p.height);
cout << "拷贝构造函数被调用" << endl;
}
即在拷贝构造中,重新给拷贝的指针变量申请一块空间
如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝出现问题
初始化列表
C++提供了初始化列表来初始化属性
语法:<构造函数>(): <属性1>(<值1>), <属性2>(<值2>) ... {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream>
using namespace std ;
class person {
public:
int a;
int b;
int c;
// 初始化列表赋值
person ( int a , int b , int c ): a ( 10 ), b ( 20 ), c ( 30 ){
}
};
int main (){
person p;
cout << "a:" << p.a << endl;
cout << "b:" << p.b << endl;
cout << "c:" << p.c << endl;
return 0 ;
}
类对象作为类成员
C++中的成员可以是另一个类的对象,称为对象成员
如
class A {};
class B {
A a;
};
该B类中有对象A作为成员,则A为对象成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <iostream>
#include <string>
using namespace std ;
class phone {
public:
string brand;
float price;
phone ( string pname ){
brand = pname;
cout << "phone构造函数被调用" << endl;
}
~phone (){
cout << "phone析构函数被调用" << endl;
}
};
class person {
public:
string name;
phone iphone;
person ( string pname , string pbrand ): name (pname), iphone (pbrand){
//相当于phone iphone = pname;的隐式转换
cout << "person构造函数被调用" << endl;
}
~person (){
cout << "person析构函数被调用" << endl;
}
};
int main (){
person p ( "小明" , "苹果" );
cout << "姓名:" << p.name << endl;
cout << "手机品牌:" << p.iphone.brand << endl;
return 0 ;
}
phone构造函数被调用
person构造函数被调用
姓名:小明
手机品牌:苹果
person析构函数被调用
phone析构函数被调用
注意构造函数的调用顺序:
当其他类对象作为本类成员,构造时先构造类对象,在构造自身
当其他类对象作为本类成员,析构时先析构自身,再析构类对象
静态成员
在成员变量与成员函数前加上关键字static,称为静态成员
分类:
静态成员变量
不属于某个对象,所有对象共享一份数据
编译阶段分配内存
类内声明,类外初始化
静态成员函数
所有成员共享同一个函数
静态成员函数只能访问静态成员变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream>
using namespace std ;
class person {
public:
static int a; // 需要类内声明,类外初始化
int c;
static void f (){
cout << "静态成员函数f调用" << endl;
a = 100 ;
// c = 200; // 静态成员函数不可以访问非静态成员变量
}
private:
static int b; // 静态成员变量也有权限
};
int person ::a = 100 ; // 在类外初始化,需要指定作用域
int person ::b = 100 ; // 也需要在类外初始化
int main (){
person p; // 栈上的数据,执行完后会被释放,故执行析构函数
cout << p.a << endl;
person p2;
p2.a = 200 ; // 数据共享,此处改变即一起改变
cout << p.a << endl; // 通过对象进行访问
cout << person ::a << endl; // 通过类名进行访问
// cout << p.b<<endl; // 不可访问
// 同样两种访问模式
p. f ();
person :: f ();
return 0 ;
}
应用:实现单例对象,即一个类只有一个类对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream>
using namespace std ;
class Singleton {
private:
static Singleton * m_insntance; // 存储该类的唯一实例
// 将构造与析构设为私有
Singleton (){}
~Singleton (){}
public:
// 删除拷贝构造与赋值运算符构造
Singleton ( const Singleton & obj ) = delete ;
Singleton & operator = ( const Singleton & ) = delete ;
// 创建该唯一实例, 只能通过该方法创建获得对象
static Singleton * getInstance (){
if ( ! m_insntance){
m_insntance = new Singleton ();
}
return m_insntance;
}
private:
string m_name;
public:
void setName ( const string & name ){
m_name = name;
}
const string & getName (){
return m_name;
}
};
Singleton * Singleton ::m_insntance = NULL ;
int main (){
// 第一次调用创建实例
Singleton * s1 = Singleton :: getInstance ();
cout << s1-> getName () << endl;
// 第二次调用使用已经创建的实例
Singleton * s2 = Singleton :: getInstance ();
cout << s1-> getName () << endl;
}
C++对象模型与this指针
成员变量与成员函数分开存储
c++中,类内成员变量与成员函数分开存储
只有非静态成员变量才属于类的对象上
对于空对象,编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <iostream>
using namespace std ;
class person {
};
class person2 {
int a;
};
class person3 {
int a;
static int b;
void f (){}
};
int main (){
person p;
person2 p2;
person3 p3;
// 占用内存为1
cout << "size of empty class: " << sizeof (p) << endl;
// 占用内存为4
cout << "size of class person2: " << sizeof (p2) << endl;
// 静态成员与非静态成员函数不属于类的对象上,故占用内存也为4
cout << "size of class person3: " << sizeof (p3) << endl;
return 0 ;
}
this指针
C++通过提供this指针解决了区分多个对象调用同一块代码分不清的问题
this指针指向被调用的成员函数所属的对象 ,不需要定义,直接使用
用途:
形参和成员变量同名时,可以用this指针来区分
在类的非静态成员函数中返回对象本身,可使用retrun *this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream>
using namespace std ;
class person {
public:
int age;
person ( int age ){
// 当形参和成员变量同名时,this 指针指向被调用成员函数所属对象
this ->age = age; // 此时this->age和定义的成员变量age是同一个
}
// 成员函数返回对象本身
person & addage ( person & p ){
this ->age += p.age;
return * this ;
}
};
int main (){
// 解决名称冲突
person p ( 18 );
person p2 ( 10 );
cout << "age:" << p.age << endl;
// 返回对象本身可以实现链式调用
p2. addage (p). addage (p). addage (p);
cout << "age of p2:" << p2.age << endl;
return 0 ;
}
空指针访问成员函数
C++允许空指针调用成员函数,但是要注意有没有用到this指针
如果用到this指针,要注意保证代码的鲁棒性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream>
using namespace std ;
class person {
public:
int age;
void show (){
cout << "this is person class" << endl;
}
void showage (){
// 防止指向空
if ( this == NULL ){
return ;
}
// 调用成员变量时,默认在前面加this->,但此时p是空指针,故指向的是空对象,this没有指向数据
cout << "age = " << age << endl;
}
};
int main (){
person * p = NULL ;
p-> show ();
p-> showage ();
return 0 ;
}
const修饰成员函数
常函数:
成员函数后加const称该函数为常函数
常函数内不可以修改成员属性
成员属性声明时加关键字mutable后,在常函数内依然可以修改
常对象:
声明对象前加const称该对象为常对象
常对象构造时需要对内部成员变量初始化(构造函数)
常对象只能调用常函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream>
using namespace std ;
class person {
public:
int age;
mutable int b; // 加上mutable也可以在常函数中修改
// this指针本身是指针常量,指针指向是不可修改的,指针指向的值是可以修改的
// 而加上const修饰后,修饰的是this指向的值,让指向的值也不可修改
// 相当于const person * const this;
void show () const {
//this->age = 100; // 此时不可修改
this ->b = 100 ;
}
void f (){}
person (){
}
};
int main (){
// 常对象
const person p;
// p.age = 100; // 常对象下不能修改值
p.b = 100 ; // 但有mutable的可以修改
// p.f() // 常对象只能调用常函数
p. show ();
return 0 ;
}
类的特殊成员函数
特殊成员函数包括默认构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数,他们均可以用以下两个关键字修饰
可平凡复制类
C++中,各种数据类型及他们的数组、指针、枚举等在内存中是连续存储的,可以使用memcpy()函数进行拷贝,称为可平凡复制类型
一个类若满足以下条件,被称为可平凡复制类,也是一种可平凡复制类型
没有虚函数
没有用户自定义的特殊成员函数
有自动生成的析构函数
所有非静态成员变量也是可平凡复制类型
拷贝、移动构造函数,拷贝、移动赋值运算符中至少有一个是未被删除的
对于继承类,其父类要满足上述条件
可以使用<type_traits>库中的is_trivially_copyable<类>::value来判断是否可拷贝
标准布局类
C++中,所有标量类型都是标准布局类型
对一个非继承类,满足以下条件被称为标准布局类
没有虚函数
所有类成员都具有相同访问属性
所有类成员都是标准布局类型
若类是继承类,则满足该类和父类中只有一个类有非静态数据成员,且类型与访问控制满足上述条件
可以使用<type_traits>库中的is_standard_;leyout<类>::value来判断是否可拷贝
友元
友元可以让一个函数或者类访问另一个类中的私有成员
友元的关键字为friend
全局函数做友元
在class中声明对应全局函数,并在最前面加friend关键词,可以声明友元
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <iostream>
using namespace std ;
class building {
friend void fir ( building & building ); // 声明该全局函数为building类的友元
public:
string sitting_room;
private:
string bedroom;
public:
building (){
sitting_room = "客厅" ;
bedroom = "卧室" ;
}
};
void fir ( building & building ){
cout << "访问公共属性:" << building.sitting_room << endl;
cout << "访问私有属性:" << building.bedroom << endl;
}
int main (){
building b;
fir (b);
}
类做友元
在class中声明对应类,并在最前面加friend关键词,可以声明友元,被声明的类可以访问原类中的私有属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <iostream>
using namespace std ;
class buildings ;
class fri {
public:
buildings * building;
fri ();
void visit ();
};
class buildings {
friend class fri ; // 声明fri类是building类的友元
public:
string sitting_room;
buildings ();
private:
string bedroom;
};
// 在类外写成员函数
buildings :: buildings (){
sitting_room = "客厅" ;
bedroom = "卧室" ;
}
fri :: fri (){
building = new buildings;
}
void fri :: visit (){
cout << "访问公共属性:" << building->sitting_room << endl;
cout << "访问私有属性:" << building->bedroom << endl; // 友元可以访问类中的私有属性
}
int main (){
fri f;
f. visit ();
}
成员函数做友元
在class中声明对应类中的成员函数,并在最前面加friend关键词,可以声明友元,被声明的成员函数可以访问原类中的私有属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include
using namespace std;
class buildings;
class fri{
public:
buildings* building;
fri(){
building = new buildings;
}
void visit(); // 让visit可以访问buildings中的私有属性
};
class buildings{
friend void fri::visit(); // 声明fri类下的visit成员函数是building类的友元
public:
string sitting_room;
buildings(){
sitting_room = "客厅";
bedroom = "卧室";
}
private:
string bedroom;
};
void fri::visit(){
cout <<"访问公共属性:" <sitting_room<bedroom<
运算符重载
运算符重载可以对已有的运算符重新定义,赋予另一种功能以适用于不同的数据类型
如我们通过写成员函数来实现两个类相加属性后返回新的对象
可以通过编译器提供的通用名称operator来重载运算符以简化运算
重载的本质调用为(以person类为例):
成员函数:person p3 = p1.operator<运算符>(p2)
全局函数:person p3 = p1.operator<运算符>(p1, p2)
注意:
运算符重载也可以实现函数重载
对于内置数据类型的表达式的运算符是不可以重载的
不要滥用运算符重载
加号运算符重载
成员函数:
person operator + ( person & p ){
person temp;
temp.a = this ->a + p.a;
temp.b = this ->b + p.b;
return temp;
}
全局函数:
person operator + ( person & p1 , person & p2 ){
person temp;
temp.a = p1.a + p2.a;
temp.b = p1.b + p2.b;
return temp;
}
左移运算符重载
// 只能全局重载左移运算符
// 若在类内重载会称为p.operator<<(cout) 即p<<cout (cout是ostream流中的对象)
// 这里先写cout即可实现cout在左边p在右边
// 返回值作为cout本身的引用,可以实现链式编程
ostream & operator << ( ostream & cout , person & p ){
cout << "a=" << p.a << endl << "b=" << p.b;
return cout;
}
递增运算符重载
// 前置++
// 这里返回引用是为了确保只对同一个数据进行操作
myint & operator ++ (){
num ++ ; // 先进行自增运算
return * this ; // 再返回自身
}
// 后置++
// 这里为了防止重复,添加一个int占位符来实现函数重载以区分前后置
myint operator ++ ( int ){
myint temp = * this ;
num ++ ;
return temp;
}
赋值运算符重载
C++编译器会额外给一个类添加赋值运算符的重载函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <iostream>
using namespace std ;
class person {
public:
int * age;
person ( int age ){
this ->age = new int (age); // 年龄放在堆区
}
// 将age内存释放
~person (){
if (age != NULL ){
delete age;
age = NULL ;
}
}
};
int main (){
person p1 ( 1 );
cout << "p1's age:" << * p1.age << endl;
person p2 ( 2 );
p2 = p1; // 赋值操作
cout << "p2's age:" <<* p2.age << endl;
}
该代码出错,由于age开到了同一块堆区内存,故在p1赋值给p2时,两者的age指针都指向同一块区域
则当p2执行完毕,执行析构函数时,将该堆区内存释放;再等p1执行完毕,执行析构函数时,将已经释放的内存重复释放了,故报错
我们需要通过重载赋值运算符来实现深拷贝
// 重载赋值运算符
person & operator = ( const person & p ){
// 首先判断是否有属性在堆区,若有先释放,再深拷贝
if (age != NULL ){
delete age;
age = NULL ;
}
age = new int ( * p.age);
return * this ;
}
关系运算符重载
实现自定义数据类型的比较
// 重载==运算符
bool operator == ( const person & p ){
if ( this ->name == p.name && this ->age == p.age){
return true ;
}
else return false ;
}
// 重载!=运算符
bool operator != ( const person & p ){
if ( this ->name == p.name && this ->age == p.age){
return false ;
}
else return true ;
}
大于小于类似
函数调用运算符重载
函数调用运算符即()
重载后的使用方式很像函数的调用,故也称为仿函数
仿函数没有固定写法
class print {
public:
//重载函数调用运算符
void operator () ( string s ){
cout << s << endl;
}
};
class add {
public:
int operator () ( int n1 , int n2 ){
return n1 + n2;
}
};
也可以用匿名函数对象进行调用
继承
有些类与其他类之间除了有共性,还有自己的特性,我们可以用继承的技术来减少重复代码优点:减少重复代码
语法:<子类>: <继承方式> <父类>
其中子类也称作派生类,父类也称基类
子类中的成员包含两部分:
一类是从基类继承过来的,一类是自己增加的成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class page {
public:
void f1 ()
{
cout << "f1" << endl;
}
void f2 (){
cout << "f2" << endl;
}
void f3 (){
cout << "f3" << endl;
}
};
class page2 : public page {
public:
void s1 (){
cout << "s1" << endl;
}
};
继承方式
即:
私有成员子类均不可访问
公共继承下:各成员权限不变
保护继承下:public变为protected
私有继承下:public与protected变为private
继承中的对象模型
父类中所有非静态成员属性都会被子类继承
而私有属性被编译器隐藏,故访问不到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream>
using namespace std ;
class fa {
public:
int a;
protected:
int b;
private:
int c;
};
class son : public fa {
public:
int d;
};
int main (){
// 结果为16,即父类中所有属性都会被继承
cout << sizeof (son) << endl;
}
继承中的构造与析构顺序
子类继承父类后,创建子类对象时,也会调用父类的构造函数
顺序:先构造父类,再构造子类,析构相反
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream>
using namespace std ;
class base {
public:
base (){
cout << "base构造函数" << endl;
}
~base (){
cout << "base析构函数" << endl;
}
};
class son : public base {
public:
son (){
cout << "son构造函数" << endl;
}
~son (){
cout << "son析构函数" << endl;
}
};
int main (){
son s;
}
base构造函数
son构造函数
son析构函数
base析构函数
同名成员处理方式
当子类和父类出现同名成员:子类的同名成员会隐藏掉所有父类中同名成员(函数重载也会被隐藏)
访问子类同名成员:直接访问
访问父类同名成员:加作用域
静态与非静态处理方式一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <iostream>
using namespace std ;
class fa {
public:
int a;
static int b;
fa (){
a = 100 ;
}
void f (){
cout << "f in fa" << endl;
}
static void ff (){
cout << "ff in fa" << endl;
}
};
class son : public fa {
public:
int a;
static int b;
son (){
a = 200 ;
}
void f (){
cout << "f in son" << endl;
}
static void ff (){
cout << "ff in son" << endl;
}
};
int fa ::b = 10 ;
int son ::b = 20 ;
int main (){
son s;
cout << "a in son:" << s.a << endl;
// 加作用域访问父类中的同名成员变量
cout << "a in fa:" << s.fa::a << endl;
s. f ();
// 加作用域调用父类中的同名成员函数
s.fa:: f ();
// 非静态同名成员访问:通过对象
cout << "static b in son:" << s.b << endl;
cout << "static b in fa:" << s.fa::b << endl;
s. ff ();
s.fa:: ff ();
// 非静态同名成员访问:通过类名
cout << "static b in son:" << son ::b << endl;
// 第一个::代表通过类名访问,第二个::代表访问父类作用域下
cout << "static b in fa:" << son :: fa ::b << endl;
son :: ff ();
son :: fa :: ff ();
}
多继承
C++允许一个类继承多个类
语法:<子类>: <继承方式> <父类1>, <继承方式> <父类2>...
同名成员出现时需要加作用域区分
实际开发时不建议使用多继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream>
using namespace std ;
class fa {
public:
int a;
fa (){
a = 100 ;
}
void f (){
cout << "f in fa" << endl;
}
};
class fa2 {
public:
int a;
fa2 (){
a = 200 ;
}
void f (){
cout << "f in fa" << endl;
}
};
class son : public fa , public fa2 {
public:
int c;
int d;
son (){
c = 300 ;
d = 400 ;
}
};
int main (){
// 多继承子类空间
cout << "size of son:" <<sizeof (son) << endl;
son s;
cout << "a in fa:" << s.fa::a << endl;
cout << "a in fa2:" << s.fa2::a << endl;
}
菱形继承
概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类
菱形继承时,两个父类拥有相同数据,需要加作用域区分
但该数据对于最后的子类来说存在两份,会浪费空间,需要利用虚继承 来解决问题
在需要虚继承的部分的访问权限前添加关键字vritual即可,此时对应父类称为虚基类
原理:此时的父类继承的是虚基类指针(vbptr),指向一个虚基类表,该表存储了指针位置以及对应偏移量,根据偏移量便可以找到继承的那唯一一份数据
缺点:虚继承占用内存很大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream>
using namespace std ;
class animal {
public:
int age;
};
// sheep 继承了 animal
class sheep : virtual public animal {};
// camel 继承了 animal
class camel : virtual public animal {};
// alpaca 继承了 sheep和camel
class alpaca : public sheep , public camel {};
int main (){
alpaca st;
// 菱形继承时,两个父类有相同数据,需要加作用域区分
st.sheep::age = 18 ;
st.camel::age = 20 ;
cout << "age of sheep: " << st.sheep::age << endl;
cout << "age of camel: " << st.camel::age << endl;
cout << "age of alpaca: " << st.alpaca::age << endl;
}
多态
分类
静态多态:函数和运算符重载属于静态多态,复用函数名;函数地址在编译阶段确定
动态多态:派生类和虚函数实现运行时多态;函数地址在运行阶段确定
### 动态多态
满足条件:
使用:父类的指针或者引用指向子类对象
原理:父类记录的也是虚基类指针,指向对应虚基类表。当子类重写后,对应指向的虚基类表被现子类覆盖,得到对应参数
优点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <iostream>
using namespace std ;
class animal {
public:
// 虚函数
virtual void speak (){
cout << "speaking" << endl;
}
};
class cat : public animal {
public:
void speak (){
cout << "cat is speaking" << endl;
}
};
// 地址在编译阶段确定函数地址,但为了让多个类输出对应,需要在运行时确定
// 当给animal中的speak变为虚函数后,便可以实现子类重写父类
void dospeak ( animal & ani ){ // 父类引用
ani. speak (); // 但调用的是animal中的speak函数
}
int main (){
cat cat;
dospeak (cat); // 父类引用指向了子类
}
在开发时,提倡开闭原则:即对扩展进行开放,对修改进行关闭
纯虚函数与抽象类
多态中,通常父类中的虚函数实现是毫无意义的,都是调用子类中的重写内容
因此可以将虚函数改为纯虚函数
语法:virtual <返回值类型> <函数名>([参数列表]) = 0;
此时这个类称为抽象类
抽象类无法实例化对象
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <iostream>
using namespace std ;
class base {
public:
virtual void func () = 0 ; // 纯虚函数
};
class son : public base {
public:
// 子类必须重写父类中的纯虚函数
virtual void func (){
cout << "func调用" << endl;
}
};
int main (){
// base b; 不允许实例化抽象类
base * b = new son;
b-> func ();
}
虚析构与纯虚析构
多态使用时,如果子类中有属性开辟到堆区,则父类指针在释放时就无法调用到子类的虚构代码,此时需要将父类中的析构函数改为虚析构或纯虚析构
可以解决父类指针释放子类对象
都需要有函数具体实现
其中,纯虚析构需要类外实现
有纯虚析构后该类便成为了抽象类
语法:virtual ~<类名>(){}或virtual ~<类名>()=0;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream>
using namespace std ;
class animal {
public:
virtual void speak () = 0 ;
// 用虚析构解决父类指针释放子类对象时释放不完全的问题
virtual ~animal (){
cout << "animal析构调用" << endl;
}
};
class cat : public animal {
public:
cat ( string name ){
cname = new string (name);
}
virtual void speak (){
cout <<* cname << "'s speaking" << endl;
}
~cat (){
if (cname != NULL ){
cout << "cat析构调用" << endl;
delete cname;
cname = NULL ;
}
}
string * cname;
};
int main (){
animal * ani = new cat ( "Tom" );
ani-> speak ();
// 父类指针析构时不会调用子类析构函数,导致若子类有析构属性时不会被释放
delete ani;
}
杂项
override与final限定符
在基类是虚函数的情况下,子类对该虚函数进行重写,在要重写的函数后面加上override可以防止因为子类的拼写错误,数据类型错误等问题,导致无法调用到子类而直接调用了基类
(若不相同会直接报错)
在虚函数声明的结尾加上final可以防止该虚函数被子类重写,在类声明的后面加上final可以防止该类被继承
注意这两个限定符不是保留关键字
const成员函数
在成员函数声明和定义的后面加上关键字const
const成员函数不能修改其成员变量,不能调用非const成员函数,不能通过const类型的对象调用非const成员函数
即常量对象只可以调用常量方法