三大特性:封装、继承、多态
访问权限
C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限,他们分别表示公有的、受保护的、私有的,被称为成员访问限定符。
在类的内部(定义类的代码内部),无论成员被声明为public、protected还是private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问public属性的成员,不能访问private、private属性的成员。
?? 无论公有继承、私有继承还是保护继承,私有成员不能被“派生类(子类)”访问,基类中的公有和保护成员能被“派生类”访问。
?? 对于公有继承,只有基类中的公有成员能被“派生类对象”访问,保护和私有成员不能被“派生类对象”访问。对于私有和保护继承,基类中的所有成员不能被派生类对象访问。
封装
成员变量私有化,提供公共的getter和setter给外界去访问成员变量。
数据和代码捆绑在一起,避免外界干扰和不确定性访问。
功能:
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
例如:将公共的数据或方法用public修饰,而不希望被访问的数据或方法采用private修饰。
继承
可以让子类拥有父类的所有成员(变量、函数)
struct Student {
int m_age;
int m_score;
void run() { }
void study() { }
};
struct Worker {
int m_age;
int m_salary;
void run() { }
void work() { }
};
// 在这里能看到Student和Worker都有 m_age 和 void run()
// 可以将他们共性的东西提取出来,特性的东西留下
// 将上述代码可以改成如下
struct Person {
int m_age;
void run() { }
}
;struct Student : Person {
// 相当于Student继承了Person类
// 将Person中所有的成员都拿过来
int m_score;
void study() { }
};
struct Worker : Person {
int m_salary;
void work() { }
};
-
关系描述
- Student是子类(subclass,派生类)
- Person是父类(superclass,超类)
-
作用
-
避免重复代码
-
Person类 :4个字节
Student类:8个字节(继承了Person类)
Worker类:8个字节 (继承了Person类)
如果定义
Worker wk;
wk.m_age = 10;
wk.m_salary = 200;
在内存中,m_age和m_salary的地址是相连的,各占4个字节,m_age在前。
从父类继承的成员变量会排布在前。
成员访问权限
成员访问权限、继承方式有三种:
public > protected > private
-
public
公共的,任何地方都可以访问(struct默认)
-
protected
子类内部、当前类内部都可以访问
-
private
私有的,只有当前类内部可以访问(class默认)
-
注意!!子类内部访问父类成员的权限,是以下2项中权限最小的那个
- 成员本身的访问权限
- 上一级父类的继承方式
一般会以public继承,因为这样可以完整地将父类原本的成员权限继承下来
struct Person {
int m_age;
};
struct Student : private Person {
// 相当于
// private:
int m_age;
};
struct Worker : Student {
// 在这里继承Student后不能够修改m_age,因为在父类中m_age属于private
int m_salary;
};
// 如果Student是以public继承Person,那么在Worker中就可以修改m_age
// 但如果Person中m_age是private,那么在Worker中就不可以修改
-
访问权限不影响对象的内存布局
就算父类的private成员不能直接访问,也可以通过public函数来间接访问。
初始化列表
一种便捷的初始化对象成员的方式
只能用在构造函数中
struct Person {
int m_age;
int m_height;
Person(int age, int height): m_age(age), m_height(height) {}
// 等价于(汇编代码完全一样)
/*Person(int age, int height) {
m_age = age;
m_height = height;
}*/
};
int main() {
Person(18, 180);
getchar();
return 0;
}
-
注意!初始化类的时候是按照变量声明的顺序!
struct Person { int m_age; int m_height; Person(int age, int height): m_height(height), m_age(m_height) {} }; int main() { Person person(18, 180); cout << person.m_age << person.m_height << endl; getchar(); return 0; }
m_age 在 m_height 之前初始化,所以在用 m_height 给 m_age 赋值的时候,m_height 还没有初始化。
所以输出为:
-858993460 180
如果是先初始化 m_height 再初始化 m_age,就可以将 m_height 的值赋给 m_age
struct Person { int m_height; int m_age; Person(int age, int height): m_height(height), m_age(m_height) {} }; int main() { Person person(18, 180); cout << person.m_age << person.m_height << endl; getchar(); return 0; }
输出为:
180 180
-
如果函数声明和实现是分离的
初始化列表只能写在函数的实现中
默认参数只能写在函数声明中
struct Person { int m_height; int m_age; Person(int age = 0, int height = 0); // 默认参数只能写在函数声明中}; Person::Person(int age, int height) m_height(height), m_age(m_height) {} // 初始化列表只能写在函数的实现中
构造函数的互相调用
struct Person {
int m_age;
int m_height;
Person() {
m_age = 0;
m_height = 0;
/*
... 还有其他功能
*/
}
Person(int age, int height) {
m_age = age;
m_height = height;
}
// 以上两个构造函数的代码有点重复,可以将第一个构造函数修改如下
/* Person() :Person(0, 0){
// 必须放在初始化列表里!!!
... 其他功能
} */
};
int main() {
Person(18, 180);
getchar();
return 0;
}
如果构造函数实现的功能包含另一个,除此之外还有其余的个性化功能,那么可以直接互相调用来减少重复代码。
-
注意!构造函数调用构造函数,必须放在初始化列表里!
Person() { Person(10, 20); // 等同于 /* Person person; person.m_age = 10; person.m_height = 20; */ // 我们想要实现的是 /* this->m_age = 10; this->m_height = 20; */ }
这样并不能实现初始化,而是又创建了一个临时的Person对象
10和20赋值给了新的person对象
父类的构造函数
-
子类的构造函数默认会调用父类的无参构造函数,先调用父类的构造函数再调用子类的构造函数
struct Person() { int m_age; Person() { cout << "Person::person" << endl; } // Person() :person(0) {} // Person(int age) :m_age(age) {} }; struct Student : Person { int m_no; Student() { cout << "Student::student" << endl; } // Student() :Student(0,0) {} // Student(int age, int no) {} }; int main() { Student student; getchar(); return 0; } // 输出 // Person::person // Student::student // 会调用两个构造函数
-
如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数
struct Person() { int m_age; Person() { cout << "Person::person" << endl; } Person(int age) { cout << "Person::Person(int age)" << endl; } }; struct Student : Person { int m_no; Student() :Person(10){ // 显式地调用了父类的有参构造函数 cout << "Student::student" << endl; } }; int main() { Student student; getchar(); return 0; } // 输出 // Person::Person(int age) // Student::student
-
如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数
struct Person() { int m_age; Person(int age) { cout << "Person::Person(int age)" << endl; } }; struct Student : Person { int m_no; Student() { // 会报错,因为父类缺少无参构造函数,必须显式调用父类的有参构造函数 cout << "Student::student" << endl; } }; int main() { Student student; getchar(); return 0; }
构造和析构顺序
struct Person() {
Person() {
cout << "Person::Person()" << endl;
}
~Pereson() {
cout << "Person::~Person()" << endl;
}
};
struct Student : Person {
Student() {
// call Person::Person
cout << "Student::Student()" << endl;
}
~Student() {
cout << "Student::~Student()" << endl;
}
};
int main() {
{
Student student;
}
getchar();
return 0;
}
// 输出
// Person::Person()
// Student::Student()
- Student 继承了父类Person,Person的代码在Student前面调用,那么各个的析构函数是以什么顺序调用的?
- 先调用子类的析构再调用父类的析构
构造:先调用父类再调用子类;析构:先调用子类析构再调用父类析构
父类指针、子类指针
父类指针可以指向子类对象的,是安全的,在开发中经常用到**(继承方式必须是public)**
( struct 默认是 public 继承,class 默认是 private 继承 )
struct Person {
int m_age;
};
struct Student : Person {
int m_score;
};
int main() {
// 父类指针 指向 子类对象
Person *p = new Student();
p->m_age = 10;
getchar();
return 0;
}
-
*p 只能访问到 m_age,即Student所继承父类的成员。因为Person本身只占4个字节,所以在指向Student的m_age中并不会越界,是安全的。
如果反过来
Student *p = (Student *) new Person(); p->m_age = 10; p->m_score = 100;
第一个赋值给m_age是没有问题的,本身就在Person对象中。
但Person对象中没有m_score的位置,这样就比较危险,会将后面内存未知的东西给覆盖掉。
所以子类指针指向父类对象是不安全的。
多态
-
同一个操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
struct Animal { void speak() { cout << "Animal::speak" << endl; } void run() { cout << "Animal::run" << endl; } }; struct Dog : Animal { // 将父类中所继承的函数拿过来改成自己的 // 重写(覆写、覆盖、override) // 返回值、函数名、参数都和父类一模一样 void speak() { cout << "Dog::speak" << endl; } void run() { cout << "Dog::run" << endl; } }; struct Cat : Animal { void speak() { cout << "Cat::speak" << endl; } void run() { cout << "Cat::run" << endl; } }; struct Pig : Animal { void speak() { cout << "Pig::speak" << endl; } void run() { cout << "Pig::run" << endl; } }; void liu(Animal *p) { p->speak(); p->run(); } int main() { liu(new Dog()); liu(new Cat()); liu(new Pig()); getchar(); return 0; }
以上代码也无法实现多态,因为默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态。
C++中的多态通过虚函数来实现。
虚函数:被virtual修饰的函数叫虚函数
即将Animal类中的函数修改为虚函数
struct Animal { virtual void speak() { cout << "Animal::speak" << endl; } virtual void run() { cout << "Animal::run" << endl; } };
只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类可以省略virtual)
- 在运行时,可以识别出真正的对象类型,调用对应子类的函数
-
多态的要素
- 子类重写父类的成员函数(override)
- 父类指针指向子类对象
- 利用父类指针调用(重写的)成员函数
虚表:虚函数的实现原理
里面存储着最终需要调用的虚函数地址,虚表也叫虚函数表。
一旦变成了虚函数,函数的占用内存会多四个字节。不是虚函数的话 cat 函数的首地址即是第一个变量的地址,变成虚函数后会增加四个字节存放虚表的地址,指向第一个虚函数的地址。
汇编代码:
// 调用speakAnimal
*cat = new Cat();
cat->speak();
// ebp-8是指针变量cat的地址
// eax是Cat对象的地址
mov eax, dword ptr [ebp-8]
// 取出Cat对象最前面的4个字节给edx
// 取出虚表的地址值给edx
mov edx, dword ptr [eax]
// 取出虚表的最前面4个字节给eax
// 取出Cat::speak的函数调用地址给eax
mov eax, dword ptr [edx]
// call Cat::speak
call eax
// 调用run
cat->run();
// ebp-8是指针变量cat的地址
// eax存储的是Cat对象的地址
mov eax, dword ptr [ebp-8]
// 取出Cat对象的最前面4个字节(虚表地址)给edx
mov edx, dword ptr [eax]
// 跳过第一个函数speak,再取出4个字节赋值给eax
mov eax, dword ptr [edx+4]
// call Cat::run
call eax
-
虚表的细节
-
所有的cat对象(不管在全局区、栈、堆)共用同一份虚表
Animal *cat1 = new Cat(); cat1->speak(); cat1->run(); Animal *cat2 = new Cat(); cat2->speak(); cat2->run();
cat1 和 cat2 所指向的虚表是一样的,因为调用的是同样的函数
-
如果父类只有一个虚函数,那么子类的虚表中也只有一个虚函数
struct Animal { void speak() { cout << "Animal::speak" << endl; } virtual void run() { cout << "Animal::run" << endl; } };
-
如果父类两个都是虚函数,但子类的定义中只有一个,那么虚表中有两个虚函数,一个是父类的虚函数一个是子类的虚函数
class Animal { public: int m_age; virtual void speak() { cout << "Animal::speak" << endl; } virtual void run() { cout << "Animal::run" << endl; } }; class Cat : public Animal { public: int m_life; void run() { cout << "Cat::run()" << endl; } } int main() { Animal *cat = new Cat(); cat->m_age = 20; cat->speak(); cat->run(); getchar(); return 0; } /* 输出 Animal::speak Cat::run() */
-
虚析构函数
存在父类指针指向子类对象的时候(有多态的时候),应该将析构函数声明为虚函数(虚析构函数)
-
delete 父类指针时,才会调用子类的析构函数,保证析构的完整性
父类的析构函数是虚函数那么子类的析构函数也是虚函数。
纯虚函数
没有函数体且初始化为0的虚函数,用来定义接口规范。
// 因为我们不知道“动物”是怎么叫的/跑的(因为不知道是具体哪个动物)
// 所以没法定义怎么叫/跑
// 在这里就不定义,speak和run就是纯虚函数
struct Animal {
virtual void speak() = 0;
virtual void run() = 0;
};
struct Dog : Animal {
void speak() {
cout << "Dog::speak" << endl;
}
void run() {
cout << "Dog::run" << endl;
}
};
struct Cat : Animal {
void speak() {
cout << "Cat::speak" << endl;
}
void run() {
cout << "Cat::run" << endl;
}
};
struct Pig : Animal {
void speak() {
cout << "Pig::speak" << endl;
}
void run() {
cout << "Pig::run" << endl;
}
};
-
抽象类
-
含有纯虚函数的类,不可以实例化(不可以创建对象)
即上述代码中的Animal,不可以创建Animal对象,只可以建立子类的对象。
-
抽象类也可以包含非纯虚函数,成员变量
struct Animal { int m_age; virtual void speak() = 0; void run() { } };
所以说只要包含纯虚函数就是抽象类。
-
如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类
// Animal为抽象类 struct Animal { virtual void speak() = 0; virtual void run() = 0; }; // Dog中只重写了父类中的其中一个纯虚函数,所以Dog也是抽象类 struct Dog : Animal { void run() { cout << "Dog::run" << endl; } }; // Teddy完全重写了父类的纯虚函数,所以Teddy不是抽象类 struct Teddy :Dog { void speak() { cout << "Teddy::speak" << endl; } void run() { cout << "Teddy::run" << endl; } };
-