
本文共 16367 字,大约阅读时间需要 54 分钟。
本文学习自 狄泰软件学院 唐佐林老师的 C++课程
17课 对象的构造(上)
18课 对象的构造(中)
19课 对象的构造(下)
20课 初始化列表的使用
21课 对象的构造顺序
22课 对象的销毁
43课 继承的概念和意义
44课 继承中的访问级别
45课 不同的继承方式
46课 继承中的构造与析构
47课 父子间的冲突
48课 同名覆盖引发的问题
49课 多态的概念和意义
17课 对象的构造(上)
问题1:对象中成员变量的初始值是多少?
如下例:
#includeclass Test{private: int i; int j;public: int getI() { return i; } int getJ() { return j; }};Test gt;int main(){ printf("gt.i = %d\n", gt.getI()); printf("gt.j = %d\n", gt.getJ()); Test t1; printf("t1.i = %d\n", t1.getI()); printf("t1.j = %d\n", t1.getJ()); Test* pt = new Test; printf("pt->i = %d\n", pt->getI()); printf("pt->j = %d\n", pt->getJ()); delete pt; return 0;}
思考1:类 是一种特殊的自定义类型,这种的特殊的自定义类型可以用来定义变量,从面向对象的角度说,就是定义对象,但是本质上从程序的角度说,就是通过一个自定义类型来定义一个变量。既然是变量,他就需要占用存储空间,而且是和C语言中变量占用存储空间是一致的。类=数据类型 对象=变量
结论1:从程序设计的角度,对象只是变量,因此:
1.1 在栈上创建对象时(局部变量),成员变量初始化为随机值 1.2 在堆上创建对象时(new),成员变量初始值为随机值 1.3 在静态存储区创建对象时(全局变量或静态变量),成员变量初始值为018课 对象的构造(中)
带有参数的构造函数
1. 构造函数可以根据需要定义参数2. 一个类中可以存在多个重载的构造函数3. 构造函数的重载遵循C++重载的规则class Test{ public: Test(int v) { //use v to initialize member }}
注意:定义对象和对象声明不一样
对象定义:申请对象的空间并调用构造函数对象声明:告诉编译器存在这样一个对象Test t; // 定义对象并调用构造函数int main(){ //告诉编译器存在名为 t 的 Test对象 extern Test t; return 0;}
构造函数的自动调用
class Test{public: Test() {} Test(int v) {}};int main(){ Test t; //调用 Test() Test t1(1); //调用Test(int v); Test t2 = 1; //调用 Test(int v); return 0;}
实验1
#includeclass Test{public: Test() { printf("Test()\n"); } Test(int v) { printf("Test(int v), v = %d\n", v); }};int main(){ Test t; // 调用 Test() Test t1(1); // 调用 Test(int v) Test t2 = 2; // 调用 Test(int v) int i(100); printf("i = %d\n", i); return 0;}
构造函数的调用
一般情况下,构造函数在对象定义时被自动调用 一些特殊情况下,需要手工调用构造函数问题: 如何创建一个对象数组?(一些特殊情况下,需要手工调用构造函数)
#includeclass Test{private: int m_value;public: Test() { printf("Test()\n"); m_value = 0; } Test(int v) { printf("Test(int v), v = %d\n", v); m_value = v; } int getValue() { return m_value; }};int main(){ Test ta[3] = {Test(), Test(1), Test(2)}; for(int i=0; i<3; i++) { printf("ta[%d].getValue() = %d\n", i , ta[i].getValue()); } Test t = Test(100); printf("t.getValue() = %d\n", t.getValue()); return 0;}
-
构造函数可以根据需要定义参数
-
构造函数之间可以存在重载关系
-
构造函数遵循C++中重载函数的规则
-
对象定义时会出发构造函数的调用
-
在一些情况下可以手动调用构造函数
19课 对象的构造(下)
两个特殊的构造函数
-
无参构造函数
:没有参数的构造函数。当类中没有定义构造函数时,编译器会默认提供一个无参构造函数,并且其函数体为空。 -
拷贝构造函数
:参数为 const class_name& 的构造函数。当类中没有定义拷贝构造函数时,编译器会默认提供一个拷贝构造函数,简单的进行成员变量的值复制
实验一
#includeclass Test{private: int i; int j;public: int getI() { return i; } int getJ() { return j; } /*Test(const Test& t) { i = t.i; j = t.j; } Test() { }*/};int main(){ Test t1; Test t2 = t1; printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ()); printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ()); return 0;}
实验二
#includeclass Test{private: int i; int j;public: int getI() { return i; } int getJ() { return j; } Test(const Test& t) { i = t.i; j = t.j; } Test() { }};int main(){ Test t1; Test t2 = t1; printf("t1.i = %d, t1.j = %d\n", t1.getI(), t1.getJ()); printf("t2.i = %d, t2.j = %d\n", t2.getI(), t2.getJ()); return 0;}
结果是一样的
拷贝构造函数的意义
- 兼容C语言的初始化方式
- 初始化行为能够符合预期逻辑
- 浅拷贝 :拷贝后对象的物理状态相同
- 深拷贝 :拷贝后对象的逻辑状态相同
编译器提供的拷贝构造函数只是进行浅拷贝
实验三
#includeclass Test{private: int i; int j; int* p;public: int getI() { return i; } int getJ() { return j; } int* getP() { return p; } Test(const Test& t) { i = t.i; j = t.j; p = new int; *p = *t.p; } Test(int v) { i = 1; j = 2; p = new int; *p = v; } void free() { delete p; }};int main(){ Test t1(3); Test t2(t1); printf("t1.i = %d, t1.j = %d, *t1.p = %d\n", t1.getI(), t1.getJ(), *t1.getP()); printf("t2.i = %d, t2.j = %d, *t2.p = %d\n", t2.getI(), t2.getJ(), *t2.getP()); t1.free(); t2.free(); return 0;}
问题:
什么时候需要进行深拷贝?答: 对象中有成员指代了系统中的资源
- 成员指向了动态内存空间
- 成员打开了外存中的文件
- 成员使用了系统中的网络端口
关于浅拷贝指针 重复free的问题 如下:
小结:
- C++ 编译器会默认提供构造函数
- 无参构造函数用于定义对象的默认初始状态
- 拷贝构造函数在创建对象时拷贝对象的状态
- 对象的拷贝有浅拷贝和深拷贝两种方式(浅拷贝指针时候,会发生重复free 的 bug)
- 浅拷贝是得对象的物理状态相同
- 深拷贝是得对象的逻辑状态相同
20课 初始化列表的使用
问题:类中是否可以定义const成员?
下面的定义是否合法?如果合法 ci 的值是什么,存储在哪里?
class Test{private: const int ci;public: int getCI() {return ci;}};
实验一
#includeclass Test{private: const int ci;public: Test() { ci = 10; } int getCI() { return ci; }};int main(){ Test t; printf("t.ci = %d\n", t.getCI()); return 0;}
- C++中提供了初始化列表对成员变量进行初始化
语法如下:
ClassName::ClassName() : m1(v1),m2(v1,v2),m3(v3){ //some other initialize operation}
注意:
- 成员的初始化顺序与成员的声明顺序相同
- 成员的初始化顺序与初始化列表中的位置无关
- 初始化列表先于构造函数的函数体先执行
实验二
#includeclass Value{private: int mi;public: Value(int i) { printf("i = %d\n", i); mi = i; } int getI() { return mi; }};class Test{private: Value m2; Value m3; Value m1;public: Test() : m1(1), m2(2), m3(3) { printf("Test::Test()\n"); }};int main(){ Test t; return 0;}
- 类中的const成员会被分配空间的
- 类中的const成员的本质是只读变量
- 类中的const成员只能在初始化列表中指定初始值
编译器无法直接得到const成员的初始值,因此无法进入符号表成为真正以意义上的常量。
实验三
#includeclass Value{private: int mi;public: Value(int i) { printf("i = %d\n", i); mi = i; } int getI() { return mi; }};class Test{private: const int ci; Value m2; Value m3; Value m1;public: Test() : m1(1), m2(2), m3(3), ci(100) { printf("Test::Test()\n"); } int getCI() { return ci; } int setCI(int v) { int* p = const_cast (&ci); *p = v; }};int main(){ Test t; printf("t.ci = %d\n", t.getCI()); t.setCI(10); printf("t.ci = %d\n", t.getCI()); return 0;}
- 初始化:对正在创建的对象进行初值设置
- 赋值:对已经存在的对象进行值设置
小结:
- 类中可以使用初始化列表对成员进行初始化
- 初始化列表先于构造函数体执行
- 类中可以定义 const 成员变量
- const 成员变量必须在初始化列表中指定初值
- const 成员变量为只读变量
21课 对象的构造顺序
问题: C++中的类可以定义多个对象,那么对象的构造顺序是怎样的
-
对于局部对象:
- 当程序执行流到达对象的定义语句时进行构造
-
对于堆对象:
- 当程序执行流达到 new 语句时创建对象
- 使用 new 创建对象将自动触发构造函数的调用
-
对于全局对象:
- 对象的构造顺序是不确定的
- 不用的编译器使用不同的规则确定构造顺序
小结:
- 局部对象的构造顺序依赖于程序的执行流
- 堆对象的构造顺序依赖 new 的使用顺序
- 全局对象的构造顺序是不确定的
22课 对象的销毁
生活中的对象都是被初始化后才上市的
生活中的对象被销毁之前会做一些清理工作问题:C++中如何清理需要销毁的对象
一般而言,需要销毁的对象都应该做清理
解决方案:-
C++的类中可以定义一个特殊的清理函数
- 这个特殊的清理函数叫做析构函数
- 析构函数的功能与构造函数相反
-
定义 : ~ClassName()
- 析构函数没有参数也没有返回值类型声明
- 析构函数在对象销毁时自动被调用
-
析构函数的定义准则
当类中自定义了构造函数,并且构造函数中使用了系统资源(如:内存申请,文件打开,等等),则需要自定义析构函数。
小结:
- 析构函数是对象销毁时进行清理的特殊函数
- 析构函数在对象销毁时自动被调用
- 析构函数是对象释放系统资源的保障
43课 继承的概念和意义
问题:类之间是否存在直接的关系连接?
组合关系:整体与部分的关系
*组合关系的特点-将其他类的对象作为当前类成的成员使用-当前类的对象与成员对象的生命周期相同-成员对象在用法上与噗通对象完全一致
继承关系:父子关系 (父类:基类 子类:派生类)
面向对象中的继承指类之间的父子关系-子类拥有父类的所有属性和行为-子类就是一种特殊的父类-子类对象可以当做父类对象使用-子类中可以添加父类没有的方法和属性
C++中通过下面的方式描述继承关系:
class Parent{ int mv;public: void method(){};};class Child : public Parent //描述继承关系{ };
重要规则:
-子类就是一个特殊的父类 -子类对象可以直接初始化父类对象 -子类对象可以直接赋值给父类对象继承是C++中代码复用的重要手段,通过继承,可以获得父类的所有功能,并且可以在子类中重写已有的功能,或者添加新功能
注意,创建子类对象的时候,会首先创建父类中的成员变量,再调用父类构造,最后调用子类构造
析构子类对象的时候,会首先析构子类对象,然后是父类,最后是父类成员变量,和构造顺序相反。 问题:为什么回调用父类的构造函数小结:
*继承是面向对象中类之间的一种关系*子类拥有父类的所有属性和行为*子类对象可以当做父类对象使用*子类可以添加父类没有的方法和属性*继承是面向对象中代码复用的重要手段
44课 继承中的访问级别
问题:子类是否可以直接访问父类的私有成员?
面向对象中的访问级别不只是public和private
可以定义protected访问级别关键字protected的意义-修饰的成员不能被外界直接访问-修饰的成员可已被子类直接访问
注意:
protected:{ protected修饰的变量无论父类对象还是子类对象都不能在外部直接访问 :类似于 a.mv等操作 a为父类或子类对象,mv为父类protected成员变量父类和子类都只能通过类内部访问protected变量 即在父类或子类中添加如下类似函数: getvalue(){ mv = 100....}}private:{ private修饰的成员变量,不能被外部直接访问,只能通过内部成员函数访问。子类没有权限}
问题:为什么面向对象中需要protected?
45课 不同的继承方式
被忽略的细节:
冒号(:)表示继承关系,Parent表示被继承的类,public的意义是什么?class Parent{ };class Child :public Parent{ };
是否可以将继承语句的public换成protected或者private? 如果可以,与public继承有什么区别?
C++中支持三种不同的继承方式
-public继承*父类成员在子类中保持原有访问级别-private继承*父类成员在子类中变为私有成员-protected继承*父类中的公有成员变为保护成员,其他成员保持不变
一般而言,C++工程项目中只是用public继承C++的派生语言语言只支持一种继承方式:public继承protected和private继承带来的复杂性远大于实用性
小结:
C++中支持3种不同的继承方式继承方式直接影响父类成员在子类中的访问属性一般而言,工程中只是用public的继承方式C++的派生语言中只支持public继承方式
46课 继承中的构造与析构
问题:
如何初始化父类成员? 父类构造函数和子类构造函数有什么关系?知识点:
子类中可以定义构造函数:子类构造函数 -必须对继承而来的成员进行初始化 1 直接通过初始化列表或赋值的方法进行初始化 2 调用父类构造函数进行初始化(通常做法)
思考:在子类中无法直接访问父类中的private成员,因此这时候在子类中直接赋值或者初始化继承而来的的成员是行不通的,所以要调用父类的构造函数进行初始化。
父类构造函数在子类中的调用方式
1 默认调用 :子类在创建对象时会自动调用父类的构造函数, 适用于无参构造函数和使用默认参数的构造函数2 显示调用:万能调用 通过初始化列表进行调用 适用于所有父类构造函数
父类构造函数的调用
class Child : public Parent{public: Child() /*隐式调用*/ { cout << "Child()"<< endl; } Child(string s) /*显示调用*/ :Parent("Parameter to Parent") { cout<< "Child() :"<< s <
例1:
#include#include using namespace std;class Parent{public: Parent(string s) { cout << "Parent(string s) : " << s << endl; }};class Child : public Parent{public: Child() { cout << "Child()" << endl; } Child(string s) : { cout << "Child(string s) : " << s << endl; }};int main(){ Child c; return 0;}
例2
#include#include using namespace std;class Parent{public: Parent() { cout << "Parent()" << endl; } Parent(string s) { cout << "Parent(string s) : " << s << endl; }};class Child : public Parent{public: Child() { cout << "Child()" << endl; } Child(string s) { cout << "Child(string s) : " << s << endl; }};int main(){ Child c; Child cc("cc"); return 0;}
而若将子类中的带参构造函数显式调用父类的带参构造函数如下例3:
例3:#include#include using namespace std;class Parent{public: Parent() { cout << "Parent()" << endl; } Parent(string s) { cout << "Parent(string s) : " << s << endl; }};class Child : public Parent{public: Child() { cout << "Child()" << endl; } Child(string s) : Parent(s) { cout << "Child(string s) : " << s << endl; }};int main(){ Child c; Child cc("cc"); return 0;}
构造规则:
1 子类对象在创建时会首先调用父类的构造函数2 执行父类构造函数再执行子类的构造函数3 父类构造函数可以被隐式调用 或者 显示调用
对象创建时构造函数的调用顺序
1 调用父类构造函数2 调用成员变量的构造函数3 调用自身的构造函数口诀:先父母 后客人 再自己
例4:
#include#include using namespace std;//该Object类中只有带参构造函数(并且参数不是默认参数),所以他的子类的构造函数必须要显式调用Object类的带参构造函数class Object{public: Object(string s) { cout << "Object(string s) : " << s << endl; }};//父类中没有无参构造函数或者默认参数的构造函数,所以Parent类构造函数要显式调用Object类的构造函数class Parent : public Object{public: Parent() : Object("Default") { cout << "Parent()" << endl; } Parent(string s) : Object(s) { cout << "Parent(string s) : " << s << endl; }};//继承+组合class Child : public Parent{ Object mO1; Object mO2;public: //由于父类有无参构造函数,所以不用显式调用父类的构造函数,但是要初始化成员变量,调用成员变量的构造函数 Child() : mO1("Default 1"), mO2("Default 2") { cout << "Child()" << endl; } //说明不调用父类的无参构造,而是显式调用父类有参构造,并且初始化成员变量 Child(string s) : Parent(s), mO1(s + " 1"), mO2(s + " 2") { cout << "Child(string s) : " << s << endl; }};int main(){ Child cc("cc"); Child aa; return 0;}
析构函数的调用顺序和构造函数相反
1 执行自身的析构函数2 执行成员变量的析构函数3 执行父类的析构函数
小结:
1 子类对象在创建时需要调用父类构造函数进行初始化2 先执行父类构造函数然后执行成员的构造函数3 父类构造函数显示调用需要在初始化列表中进行4 子类对象在销毁时需要调用父类析构函数进行清理5 析构顺序与构造顺序对称相反
47课 父子间的冲突
问题:子类中是否可以定义父类中的同名成员?如果可以。如何区分?如果不可以,为什么?
如下:
#include#include using namespace std;class Parent{public: int mi;};class Child : public Parent{public: int mi;};int main(){ Child c; c.mi = 100; // mi 究竟是子类自定义的,还是从父类继承得到的? return 0;}
1 子类可以定义父类中的同名成员
2 子类中的成员将隐藏父类中的同名成员 3 父类中的同名成员依然存在于子类 4 通过作用域分便符(::)访问父类中的同名成员访问父类中的同名成员:
Child c;c.mi = 100; //子类中的mic.Parent::mi = 1000; //父类中的mi
注意:名字虽然相同,但是作用于不同,在不同的命名空间。
回顾:类中的成员函数可以进行重载
1 重载函数的本质为多个不同的函数2 函数名和参数列表是唯一的标识3 函数重载必须发生在同一个作用域
问题:子类中定义的函数是否能够重载父类中的同名函数?
答案:由于作用域不同 ,所以不能重载,但是会发生覆盖,子类中的同名函数会隐藏父类中的同名函数。1 子类中的函数将隐藏父类的同名函数2 子类无法重载父类中的成员函数3 使用作用域分辨符访问父类中的同名函数4 子类可以定义父类中完全相同的成员函数
例1:
#include#include using namespace std;class Parent{public: int mi; void add(int v) { mi += v; } void add(int a, int b) { mi += (a + b); }};class Child : public Parent{public: int mi; void add(int v) { mi += v; } void add(int a, int b) { mi += (a + b); } void add(int x, int y, int z) { mi += (x + y + z); }};int main(){ Child c; c.mi = 100; c.Parent::mi = 1000; cout << "c.mi = " << c.mi << endl; cout << "c.Parent::mi = " << c.Parent::mi << endl; c.add(1); c.add(2, 3); c.add(4, 5, 6); cout << "c.mi = " << c.mi << endl; cout << "c.Parent::mi = " << c.Parent::mi << endl; return 0;}
小结:
1 子类可以定义父类中的同名成员2 子类中的成员将隐藏父类中的同名成员3 子类和父类中的函数不能构成重载关系4 子类可以定义父类中完全相同的成员函数5 使用作用域分辨符访问父类中的同名成员
48课 同名覆盖引发的问题
一 父子间的赋值兼容
二 特殊的同名函数
三 当函数重写遇到赋值兼容
1 父子间的赋值兼容
定义1:
-子类对象可以当做父类对象使用(兼容)1 子类对象可以直接赋值给父类对象2 子类对象可以直接初始化父类对象3 父类指针可以直接指向子类对象4 父类引用可以直接引用子类对象
如下例:
#include#include using namespace std;class Parent{public: int mi; void add(int i) { mi += i; } void add(int a, int b) { mi += (a + b); }};class Child : public Parent{public: int mv; void add(int x, int y, int z) { mv += (x + y + z); }};int main(){ Parent p; Child c; p = c; Parent p1(c); Parent& rp = c; Parent* pp = &c; rp.mi = 100; rp.add(5); // 没有发生同名覆盖? rp.add(10, 10); // 没有发生同名覆盖? /* 为什么编译不过? */ // pp->mv = 1000; // pp->add(1, 10, 100); return 0;}
结论:当使用父类指针(引用)指向子类对象时
1 子类对象退化为父类对象2 只能访问父类中定义的成员3 可以直接访问被子类覆盖的同名成员
2 特殊的同名函数
定义1:
1 子类中可以重定义父类中已经存在的成员函数2 这种重定义发生在继承中,叫做函数重写3 函数重写是同名覆盖的一种特殊情况
注意:
若此时操作:Child c;Parent* p = &c;p->print();//在执行这条语句时候,p所指向的c对象退化为父类对象,因此此p指向父类的print(),但是执行完该语句,c还是子类对象。
3 当函数重写遇到赋值兼容
问题:当函数重写遇到赋值兼容会发生什么?
#include#include using namespace std;class Parent{public: int mi; void add(int i) { mi += i; } void add(int a, int b) { mi += (a + b); } void print() { cout << "I'm Parent." << endl; }};class Child : public Parent{public: int mv; void add(int x, int y, int z) { mv += (x + y + z); } void print() { cout << "I'm Child." << endl; }};void how_to_print(Parent* p){ p->print();}int main(){ Parent p; Child c; how_to_print(&p); // Expected to print: I'm Parent. how_to_print(&c); // Expected to print: I'm Child. return 0;}
问题分析:
在编译期间,编译器只能根据指针的类型判断所指向的对象根据赋值兼容,编译器认为父类指针指向的是父类对象因此,编译结果只可能是调用父类中定义的同名函数
void how_to_print(Parent* p)
{ p->print(); } 在编译这个函数的时候,编译器不可能知道指针p究竟指向了什么。但是编译器没有理由报错,于是,根据指针类型判断所执行的对象,编译器认为最安全的做法是调用父类的print函数,因为父类和子类肯定都有相同的printf函数。编译的处理方式是合理的吗?是期望的吗?
小结:
1 子类对象可以当做父类对象使用(赋值兼容)2 父类指针可以正确的指向子类对象3 父类引用可以正确的代表子类对象4 子类中可以重写父类中的成员函数
49课 多态的概念和意义
函数重写回顾:
1 父类中被重写的函数依然会继承给子类2 子类中重写的函数将覆盖父类中的函数3 通过作用域分辨符(::)可以访问到父类中的函数Child c;Parent* p = &c;c. Parent::print();//从父类继承c.print();//在子类重写p->print();//从父类调用
函数重写的原因:
父类中的函数版本不能满足我们子类的需求,子类中需要重新定义一个全新的函数,我们希望只要是子类的对象,那么调用的都是子类中的函数版本,而不是父类中的版本。面向对象中期望的行为
1 根据实际的对象类型判断如何调用重写函数2父类指针(引用)指向 1 父类对象则调用父类中定义的函数 2 子类对象则调用子类中定义的重写函数
面向对象中多台的概念:
1 根据实际的对象类型决定函数调用的具体目标2 同样的调用语句在实际运行时有多种不同的表现形态
C++语言直接支持多态的概念
1 通过使用virtual关键字对多态度进行支持 2 被virtual声明的函数被重写后具有多态特性 3 被virtual声明的函数叫做虚函数例1
#include#include using namespace std;class Parent{public: virtual void print() { cout << "I'm Parent." << endl; }};class Child : public Parent{public: void print() { cout << "I'm Child." << endl; }};void how_to_print(Parent* p){ p->print(); // 展现多态的行为}int main(){ Parent p; Child c; how_to_print(&p); // Expected to print: I'm Parent. how_to_print(&c); // Expected to print: I'm Child. return 0;}
多态的意义:
1 在程序运行过程中展现出动态的特性2 函数重写必须多态实现,否则没有意义3 多态饰面向对象组件化程序设计的基础特性
理论中的概念
1 静态联编 在程序的编译期间就能确定具体的函数调用,比如函数重载2 动态联编
在程序实际运行后才能确定具体的函数调用,比如函数重写小结:
1 函数重写只可能发生在父类与子类之间2 根据实际对象的类型确定调用的具体函数3 virtual关键字是C++中支持多态的唯一方式4 被重写的虚函数可表现出多态的特性
=======================================================================================
发表评论
最新留言
关于作者
