构造函数的调用规则
默认情况下,C++编译器至少给一个类添加三个函数
默认构造函数(无参、函数体为空)
默认析构函数(无参、函数体为空)
默认拷贝函数构造函数,对全部属性的值拷贝
构造函数调用规则如下:
如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,C++不会再提供其他构造函数
即:
用户提供了有参,编译器不会提供无参,但会提供拷贝;
用户提供了拷贝,编译器什么构造函数都不会提供。
深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝操作。
- 深拷贝:在堆区中重新申请空间,进行拷贝操作。
#include<iostream> using namespace std; class Person { public: int m_Age; int* m_Height; Person() { cout << "Person的默认构造函数调用" << endl; } Person(int age,int height) { m_Height = new int(height); //把身高开辟到堆区 m_Age = age; cout << "Person的有参构造函数调用" << endl; } Person(const Person& p) { cout << "Person的拷贝构造函数调用" << endl; m_Age = p.m_Age; m_Height = p.m_Height; //编译器默认实现的就是这行代码 } ~Person() { //将堆区开辟的数据进行释放 if (m_Height !=NULL) { delete m_Height; m_Height = NULL; } cout << "Person的析构构造函数调用" << endl; } }; void foo() { Person p1(18,166); cout << p1.m_Age<<" " << *p1.m_Height << endl; Person p2(p1); cout << p2.m_Age<<" " <<*p2.m_Height<< endl; } int main(void) { foo(); return 0; }
此时这样执行会发生报错,内存重复释放。
解决:编译器提供的浅拷贝不行了,我们自己深拷贝重新开辟一块空间:
Person(const Person& p) { cout << "Person的拷贝构造函数调用" << endl; m_Age = p.m_Age; //m_Height = p.m_Height; //编译器默认实现的就是这行代码 m_Height = new int(*p.m_Height); }
结果:
/* Person的有参构造函数调用 18 166 Person的拷贝构造函数调用 18 166 Person的析构构造函数调用 Person的析构构造函数调用 */
即:如果有属性在堆区开辟的,一定要自己提供拷贝构造函数新开辟一块空间,防止浅拷贝在析构函数释放内存时带来的问题。
初始化列表
了解一下就行。
作用:
C++提供了初始化列表语法,用来初始化对象。
语法:
构造函数():属性1(值1),属性2(值2)…{}
示例:
#include<iostream> using namespace std; class Person { public: //传统赋值操作 /*Person(int a, int b, int c) { m_A = a; m_B = b; m_C = c; }*/ //初始化列表初始化属性 Person(int a,int b,int c) :m_A(a), m_B(b), m_C(c){} int m_A; int m_B; int m_C; }; void test() { //Person p(10,20,30); Person p(30,20,10); cout << p.m_A << endl; cout << p.m_B << endl; cout << p.m_C << endl; } int main(void) { test(); system("pause"); return 0; }
类对象作为类成员
C++中类的成员可以是另一个类的对象,我们称该成员为对象成员。
如:
B类中有对象A作为成员,A为对象成员。
那么当创建B对时,A先被构造,当其他类的对象作为本类的成员时,构造时先构造其他类的对象,再构造自身。
析构则与构造函数相反。自身的析构函数先进行,之后其它类再进行。
#include<iostream> #include<string.h> using namespace std; class Student{ public: int id; string name; Student() {}; Student(int i, string n) { id = i; name = n; } }; class Teacher { public: int id; string name; Student s; Teacher(int i, string n, Student stu) { id = i; name = n; s = stu; } }; int main(void) { Student s(001, "张三"); Teacher t(001, "李老师", s); cout << t.s.name << endl; // 张三 return 0; }
Teacher(int i, string n, Student stu) 这行要求Student有默认的无参构造函数,因为我写了有参的,系统不再提供无参,所以要自己手动加一个。当然如果Teacher(int i, string n, Student stu = Student(007, "张三sss")) 弄个默认的就不用。
#include<iostream> #include<string.h> using namespace std; class Student{ public: int id; string name; Student() {}; Student(int i, string n) { id = i; name = n; } }; class Teacher { public: int id; string name; Student s; Teacher(int i, string n, Student stu = Student (001, "张三")) { id = i; name = n; s = stu; } }; int main(void) { Student s(001, "张三"); Teacher t1(001, "李老师", s); Teacher t2(001, "王老师"); cout << t1.s.name << endl; // 张三 cout << t2.s.name << endl; // 张三 return 0; }
此时在Teacher(int i, string n, Student stu = Student (001, "张三")) {接收时,不能写Student stu(001, "张三") 而是Student stu = Student (001, "张三")。
当然也可以用隐式转换法去比如用一个字符串赋值给对象将它进行实例化。
静态成员
静态成员就是在成员变量和成员函数前面加上关键字static,称为静态成员。
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- 静态成员函数
- 所有对象成员共享同一个函数
- 静态成员函数只能访问静态成员变量,非静态无法访问。
class Person { public: static void func() { cout << "ststic 函数调用" << endl; } }; int main(void) { // 通过对象访问 Person p; p.func(); // ststic 函数调用 // 通过类访问 Person::func(); // ststic 函数调用 // 通过类名访问,说明不属于某个对象。参考Python的类成员。 return 0; }
对于静态成员变量,类内声明类外初始化。非静态的其实可以类内初始化;
#include<iostream> #include<string.h> using namespace std; class Person { public: int b = 20; // 就相当于默认值了,愿意拷贝构造重新赋值就重新赋,愿意改就改。 static int a; // static int a = 100; 飘红 static void func() { a = 99; } }; int Person::a=20; // 不写的话下面p.a引发报错。 int main(void) { Person p; cout << p.b << endl; // 1 return 0; }
静态成员函数访问非静态成员变量
class Person { public: static int a; // 类内声明 类外初始化 static void func() { // x = 0; 非静态成员变量无法访问 a = 99; // 静态成员函数可以访问静态成员变量 cout << "ststic 函数调用" << endl; } void xx() { a = 33; } }; int Person:: a = 100;// 类内声明 类外初始化 int main(void) { Person p; cout << p.a << endl; // 100 p.func(); cout << p.a << endl; // 99 p.xx(); cout << p.a << endl; // 33 return 0; }
为什么静态成员函数只能访问静态成员变量,非静态无法访问?
我们上面知道该static静态方法是类成员,对于非静态成员只有进行实例化对象的时候才能对其进行赋值或者调用。
假如你有p1,p2两个对象,调用这个静态函数让他去改变非静态成员变量,他自己也不知道该改变谁的非静态变量。Person::func(); p1.func(); p2.func();
当然如果静态成员函数是私有的,类外一样访问不到。
C++对象模型
在C++中,类内的成员变量和成员函数分开存储,
只有非静态成员变量才属于类的对象上。
(只有非静态成员变量的大小算进类的大小中,其他的都不算。)
C++空对象的大小是1,为的是区分不同类在内存中的占用位置。
每个空对象也应该有一个独一无二的字节。
#include<iostream> #include<string.h> using namespace std; class Person { }; int main(void) { Person p; cout << sizeof(p) << endl; // 1 return 0; }
只有非静态变量时
class Person { int a; float b; }; int main(void) { Person p; cout << sizeof(p) << endl; // 4+4=8 return 0; }
只有非静态函数时
class Person { void func() { int a = 99; } int func1() { int b = 20; return 10; } }; int main(void) { Person p; cout << sizeof(p) << endl; // 1 return 0; }
有静态成员
class Person { static int a; static void func() { a = 99; } }; int Person::a=20; int main(void) { Person p; cout << sizeof(p) << endl; // 1 return 0; }
即 在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。
this指针
我们上面知道C++成员变量和成员函数是分开存储的
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用一块代码。
那么问题是:这一块代码是如何区分是哪个对象调用自己的呢?
C++通过提供特殊的对象指针,this指针,解决上述问题。(这里的this指针可以参考JavaScript。)
this指针指向被调用的成员函数所属的对象。谁调的,this就指向谁。
this指针是隐含每个非静态成员函数内的一种指针。
this指针的用途
当形参和成员变量同名时,可用this指针来区分(这也是为什么我说不像python一样能起同样的名字赋值)
在类的非静态成员函数中返回对象本身,可使用return *this
解决名称冲突
#include<iostream> #include<string.h> using namespace std; class Person { public: int age; Person(int age) { this->age = age; } Person(const Person &p) { age = p.age; cout << "拷贝构造函数" << endl; } Person func() { return *this; } }; int main(void) { Person p(99); // 这里不可写Person p; 默认构造函数已经无了。 cout << p.age << endl; // 99 Person p1 = p.func(); // 复习:这里其实又调用了拷贝构造函数 cout << p1.age << endl; // 99 return 0; }
cout << this << endl;是一块地址 *this是调用的对象
Person(int age) { this->age = age; // *this->age = age; 飘红 }
this指针的本质是指针常量,指针的指向是不可以修改的 Person *const this;
复习:拷贝构造函数(看好是哪步调用的)
Person func() { return *this; } }; int main(void) { Person p(99); cout << p.age << endl; // 99 p.func(); // 调用拷贝构造函数 生成这里的匿名对象 直接释放 return 0; }
Person& func() { return *this; } }; int main(void) { Person p(99); cout << p.age << endl; p.func(); // 不调用拷贝构造函数 return 0; }
Person& func() { return *this; } }; int main(void) { Person p(99); cout << p.age << endl; Person p1 = p.func(); cout << p1.age << endl; // 调用拷贝构造函数 此时没有用与函数匹配的&去接收 return 0; }
Person& func() { return *this; } }; int main(void) { Person p(99); cout << p.age << endl; Person &p1 = p.func(); cout << p1.age << endl; // 不调用拷贝构造函数 return 0; }
Person func() { return *this; } }; int main(void) { Person p(99); cout << p.age << endl; Person p1 = p.func(); // 匿名对象赋值给p1只调用一次拷贝构造函数 cout << p1.age << endl; return 0; }
空指针调用成员函数
#include<iostream> using namespace std; class Person { public: void ShowClassName() { cout << "this is Person class" << endl; } void ShowPersonAge() { //提高健壮性,空的就直接返回,防止代码崩溃 if (this == NULL) { return; } //报错原因是因为传入的指针是NULL——无中生有,用一个空指针访问里面的属性 cout << this->m_Age << endl; } int m_Age; }; void test() { Person* p = NULL; p->ShowClassName(); p->ShowPersonAge(); } int main(void) { test(); system("pause"); return 0; }
const修饰成员函数
常函数:
- 成员函数后加const后我们称这个函数为常函数
- 常函数不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前const称该对象为常对象。
- 常对象只能调用常函数。
#include<iostream> using namespace std; class Person{ public: //this指针的本质是指针常量,指针的指向是不可以修改的 //就相当于Person *const this; //在成员函数后面加const修饰的是this指向,让指针指向的值也不可以修改 void showPerson() const//加个const就不允许修改了 const Person *const this; { this->m_b = 100; //this = NULL;tbhis指针是不可以修改指针的指向的 } int m_a; mutable int m_b;//加了mutable修饰的特殊变量,即使在常函数,常对象中,也可以修改这个值 void func() { m_a = 100;//在普通成员函数中是可以修改的 } }; void test() { Person P; P.showPerson(); } //常对象 void test1() { const Person p;//在对象前加const,变为常对象 //p.m_a = 100; 不可改 p.m_b = 100; //常对象只能调用常函数 p.showPerson(); //p.func();常对象不能调用普通成员函数,因为普通成员函数可以修改属性。 } int main(void) { test(); system("pause"); return 0; }
友元
在程序中,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元技术。
友元的目的就是让一个函数或者类 访问另一个类中的私有元素。
友元的关键字 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
- 全局函数做友元
#include<iostream> #include<string> using namespace std; class Building { //goodgay全局函数是Building类的一个好朋友,可以访问私有成员。 friend void goodgay(Building* building); public: Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } public: string m_SittingRoom; private: string m_BedRoom; }; //全局函数 void goodgay(Building* building) // 这不为了前面讲的节省空间吗 { cout << "好基友全局函数正在访问你的" << building->m_SittingRoom << endl; cout << "好基友全局函数正在访问你的" << building->m_BedRoom << endl; } void test() { Building building; goodgay(&building); } int main(void) { test(); system("pause"); return 0; }
类做友元
#include<iostream> #include<string> using namespace std; //在前面先声明一下 class Building; class GoodGay { public: GoodGay(); public: void visit();//参观函数 访问Building中的属性 Building* building; }; class Building { //GoodGay是Building类的好朋友,可以访问其私有属性 friend class GoodGay; public: Building(); public: string m_SittingRoom; private: string m_BedRoom; }; //在类外写成员函数 Building::Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } GoodGay::GoodGay() { //创建一个Building对象 building = new Building; } void GoodGay::visit() { cout << "好基友正在访问你的" << building->m_SittingRoom << endl; cout << "好基友正在访问你的" << building->m_BedRoom << endl; } void test() { GoodGay gy; gy.visit(); } int main(void) { test(); system("pause"); return 0; }
成员函数做友元
#include<iostream> #include<string> using namespace std; class Building; class GoodGay { public: GoodGay(); void visit();//可以访问Building中私有成员 void visit1();//不可以访问Building中私有成员 Building* builidng; }; class Building { //告诉编译器 GoodGay类中的visit成员函数作为本类的好朋友,可以访问私有函数 friend void GoodGay::visit(); public: Building(); public: string m_SittingRoom; private: string m_BedRoom; }; Building::Building() { m_SittingRoom = "客厅"; m_BedRoom = "卧室"; } GoodGay::GoodGay() { builidng = new Building; } void GoodGay::visit() { cout << "visit正在访问" << builidng->m_SittingRoom << endl; cout << "visit正在访问" << builidng->m_BedRoom << endl; } void GoodGay::visit1() { cout << "visit1正在访问" << builidng->m_SittingRoom << endl; } void test() { GoodGay gg; gg.visit(); gg.visit1(); } int main(void) { test(); system("pause"); return 0; }
继承
class Person { // 父类 }; class boy :public Person { // 子类 };
public 类内可以访问 类外可以访问 子类可以访问
private 类内可以访问 类外不可以访问 子类不可以访问
protected 类内可以访问 类外不可以访问 子类可以访问
父类中所有的非静态成员属性都会被子类继承下去。
父类中私有的成员属性是被编译器给隐藏了,因此访问不到,但是确实被继承下去了
继承中构造和析构的顺序
子类继承父类后,当创建子类时,也会调用父类的构造函数。
继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象和类名)。
同名访问:父类加作用域
//同名成员属性处理方式 void test01() { Son son; cout <<son.m_A<< endl; //如果要通过子类对象访问到父类中的同名成员,需要加作用域。 cout <<son.Base::m_A<< endl; } //同名成员函数处理方式 void test02() { Son son1; son1.func();//子 son1.Base::func();//父 //如果子类中出现和父类同名的成员函数 //子类的同名成员会隐藏掉父类中所有同名成员函数 //如果想要访问到父类中被隐藏的同名成员函数,需要加作用域 son1.Base::func(10); } int main(void) { test02(); system("pause"); return 0; }
多态
多态分为两种
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
静态多态和动态多态的区别
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
#include<iostream> using namespace std; class Animal { public: //加上virtual变成虚函数,实现地址晚绑定 virtual void speak() { cout << "动物在说话" << endl; } }; class Cat :public Animal { public: void speak() { cout << "小猫在说话" << endl; } }; class Dog : public Animal { public: void speak() { cout << "小狗在说话" << endl; } }; //地址早绑定,在编译阶段就确定函数地址 //如果想让猫说话,那么这个函数的地址就不能提前绑定,需要在运行阶段进行绑定 //动态多条满足条件 /* 1.有继承关系 2.子类重写父类的虚函数 */ //重写要求:函数返回值类型 函数名 参数列表 完全相同 //动态多态的使用 /* 父类的指针或者引用 指向子类的对象//Animal &animal = cat; */ void doSpeak(Animal& animal)//Animal &animal = cat; { animal.speak(); } void test01() { Cat cat; doSpeak(cat); Dog dog; doSpeak(dog); } int main(void) { test01(); system("pause"); return 0; }
假如我不加virtual,则cout出的结果只有:动物在说话
原理
我们打印下Animal实例化对象的大小:
int main(void) { Animal a; cout << sizeof(a) << endl; // 4 return 0; }
输出结果为4,当然如果你选择x64架构结果应该为8。
我们知道正常一个空对象其大小为1,这里的4或8实际上为一个指针的大小。它多了个vfptr,虚函数(表)指针。
简单说下就是由于子类继承父类的时候,继承父类的全部,所以因为父类写了virtual,多了个vfptr指针,继承下来的时候指针指向自己了如猫类狗类,所以在void doSpeak(Animal& animal)接收一个猫或者狗对象的时候指针就会指向它自己,而不再是父类Animal。
纯虚函数和抽象类
在多态中,通常父类汇中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
无法实例化对象
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include<iostream> using namespace std; class Animal { public: //加上virtual变成虚函数,实现地址晚绑定 virtual void speak() = 0; }; class Cat :public Animal { public: virtual void speak() // 要加上这个 { cout << "小猫在说话" << endl; } }; class Dog : public Animal { public: virtual void speak() // 要加上这个 { cout << "小狗在说话" << endl; } }; void doSpeak(Animal& animal) { animal.speak(); } void test01() { Cat cat; doSpeak(cat); Dog dog; doSpeak(dog); } int main(void) { test01(); return 0; }
此时如果执行 Animal a;报错,抽象类无法实例化对象。
虚析构和纯虚析构
问题:多态使用的时候,如果子类中有属性开辟到堆区,那么父类指针在释放的时无法调用到子类的析构代码
解决方法:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯析构作用:
可以解决父类指针释放子类对象,
都需要有具体的含函数实现
虚析构和纯虚构的区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法;
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;//声明
类名::~类名(){}
#include<iostream> #include<string> using namespace std; class Animal { public: Animal() { cout << "Animal的构造函数调用" << endl; } //利用虚析构可以解决父类指针释放对象时不干净的问题 /*virtual ~Animal() { cout << "Animal的析构函数调用" << endl; }*/ //纯虚析构,需要声明也需要实现 //有了纯虚析构之后,这个类也属于抽象类,无法实例化对象 virtual ~Animal() = 0; virtual void speak() = 0; }; //纯虚析构函数 Animal::~Animal() { cout << "Animal纯析构函数调用" << endl; } class Cat :public Animal { public: Cat(string name) { m_Name = new string(name); } virtual void speak() { cout << "Cat的构造函数调用" << endl; cout << *m_Name << "小猫在说话" << endl; } ~Cat() { if (m_Name != NULL) { cout << "Cat的析构函数调用" << endl; delete m_Name; m_Name = NULL; } } string* m_Name; }; void test01() { Animal* animal = new Cat("Tom"); /* 父类的指针在析构的时候,不会调用子类中的析构函数, 导致子类如果有堆区属性,会出现内存的泄漏情况。 解决:将父类的析构函数改为虚析构 */ animal->speak(); delete animal; // 这个要加 不然看不见打印出来的析构函数 } int main(void) { test01(); return 0; } /* Animal的构造函数调用 Cat的构造函数调用 Tom小猫在说话 Cat的析构函数调用 Animal纯析构函数调用 */
这里我们猫类中string* m_Name接收了new string(name),所以需要释放,正常情况下子类的析构函数是可以走的,但此时声明对象时父类指针在堆区,delete它自己影响不到子类。
ps:以下不写虚析构都能正常释放
void test01() { Cat c("Tom"); Animal &animal = c; // 这种写法Animal &animal = Cat("Tom")报错 animal.speak(); /* Cat c("Tom"); Animal* animal = &c; animal->speak(); */ } int main(void) { test01(); return 0; }
提问:Animal* animal = &Cat("Tom");这样写可以吗? 答不可以。
此外 子类中有this,父类指针也无法指向自身。