一、前言
最近刚好有空,趁这段时间,复习一下C++语言
,进一步夯实基础,为以后的底层开发
、音视频开发
、跨平台开发
、算法
等方向的进一步学习埋下伏笔
我们在上一篇文章中,已经充分说明,C++语言是对C的扩展,建立在对C语言知识掌握的基础上学习C++是事半功倍的\
如果你对C语言已经淡忘,或者没有学过C语言,且一时半会没有思路如何筛选可靠的C语言学习资料,可以借鉴我的这几篇文章:
1. C语言核心知识
- 01-复习C语言核心知识|综述
- 02-复习C语言核心知识|基本语法、数据类型、变量、常量、存储类、基本语句(判断语句、循环语句、go to语句)和运算
- 03-复习C语言核心知识|函数、作用域规则、数组、枚举、字符与字符串、指针
- [04-复习C语言核心知识|结构体、共用体、位域、输入&输出、文件读写]
- 05-复习C语言核心知识|预处理、头文件、强制类型转换、错误处理、递归、内存管理
面向对象常见知识点:
- 类
- 对象
- 成员变量、成员函数
- 封装、继承、多态
- .....
二、编程规范、类和对象
1. C++编程规范
- 每个人都可以有自己的编程规范,没有统一的标准,没有标准答案,没有最好的编程规范
变量名规范参考:
- 全局变量:g_
- 成员变量:m_
- 静态变量:s_
- 常量:c_
- 使用驼峰标识
2. 类和对象的基本概念
2.1 C和C++中struct区别
- C语言struct只有变量
- C++语言struct 既有变量,也有函数
2.2 类的封装
2.2.1 聚合属性和行为
- 我们编写程序的目的是为了解决现实中的问题,而这些问题的构成都是由
各种事物组成
我们在计算机中要解决这种问题,首先要做就是要将这个问题的参与者:
事和物
抽象到计算机程序中- 也就是 用程序语言表示现实的事物
那么现在问题是如何用程序语言来表示现实事物?
现实世界的事物所具有的共性就是,每个事物都具有
- 自身的属性
- 一些自身具有的行为
- 所以如果我们能把事物的属性和行为表示出来,那么就可以抽象出来这个事物
比如我们要表示人这个对象,在C语言中,我们可以这么表示:
typedef struct _Person{ char name[64]; int age; }Person; typedef struct _Aninal{ char name[64]; int age; int type; //动物种类 }Ainmal; void PersonEat(Person* person){ printf("%s在吃人吃的饭!\n",person->name); } void AnimalEat(Ainmal* animal){ printf("%s在吃动物吃的饭!\n", animal->name); } int main(){ Personperson; strcpy(person.name, "小明"); person.age = 30; AnimalEat(&person); return EXIT_SUCCESS; }
- 定义一个结构体用来表示一个对象所包含的属性,函数用来表示一个对象所具有的行为,这样我们就表示出来一个事物
C语言
的属性
和行为
- 在C语言中,行为和属性是分开的,也就是说吃饭这个属性不属于某类对象,而属于所有的共同的数据
- 所以不单单是PeopleEat可以调用Person数据,AnimalEat也可以调用Person数据,那么万一调用错误,将会导致问题发生。
聚合属性和行为
- 从这个案例我们应该可以体会到,属性和行为应该放在一起,一起表示一个具有属性和行为的对象
- 这个聚合操作,我们用专业术语封装来表述
2.2.2 封装
假如某对象的某项属性不想被外界获知,比如说
- 漂亮女孩的年龄不想被其他人知道,那么年龄这条属性应该作为女孩自己知道的属性
- 或者女孩的某些行为不想让外界知道,只需要自己知道就可以
- 那么这种情况下,封装应该再提供一种机制能够给属性和行为的访问权限控制住。
所以说封装特性包含两个方面
- 一个是属性和变量合成一个整体
- 一个是给属性和函数增加访问权限
面向对象特性|封装
-
- 把变量(属性)和函数(操作)合成一个整体,封装在一个类中
-
- 对变量和函数进行访问控制
-
访问权限
-
- 在类的内部(作用域范围内)\
没有访问权限之分,所有成员可以相互访问
-
- 在类的外部(作用域范围外)\
访问权限才有意义:
public
,private
,protected
-
- 在类的外部\
只有
public
修饰的成员才能被访问\
在没有涉及继承与派生时,private
和protected
是同等级的,外部不允许访问 案例:
//封装两层含义 //1. 属性和行为合成一个整体 //2. 访问控制,现实事物本身有些属性和行为是不对外开放 class Person { //人具有的行为(函数) public: void Dese() { cout << "我有钱,年轻,个子又高,就爱嘚瑟!" << endl; } //人的属性(变量) public: int mTall; //多高,可以让外人知道 protected: int mMoney; // 有多少钱,只能儿子孙子知道 private: int mAge; //年龄,不想让外人知道 }; int main(){ Person p; p.mTall = 220; //p.mMoney 保护成员外部无法访问 //p.mAge 私有成员外部无法访问 p.Dese(); return EXIT_SUCCESS; }
-
成员访问权限|总结
成员访问权限、继承方式有3种
- public:公共的,任何地方都可以访问(struct默认)
- protected:子类内部、当前类内部可以访问
- private:私有的,只有当前类内部可以访问(class默认)
子类内部访问父类成员的权限,是以下2项中权限最小的那个
- 成员本身的访问权限
- 上一级父类的继承方式
- 开发中用的最多的继承方式是public,这样能保留父类原来的成员访问权限
- 访问权限不影响对象的内存布局
封装Tips
- 成员变量私有化,提供公共的getter和setter给外界去访问成员变量
- 成员变量私有化,提供公共的getter和setter给外界去访问成员变量
2.2.3 struct和class的区别?
class A{
int mAge;
};
struct B{
int mAge;
};
void test(){
Aa;
Bb;
//a.mAge; //无法访问私有成员
b.mAge; //可正常外部访问
}
- 在 C++ 中, struct 和 class 都表示类,但是它们不太相同:
- class 默认访问权限为 private
- struct 默认访问权限为 public
- 建议一般用class来做类的声明标识符,在有需要的时候,才通过访问权限来对外暴露属性,以确保类的封装性
2.3 将成员变量设置为private
-
- 可赋予客户端访问数据的一致性
- 如果成员变量不是public,客户端唯一能够访问对象的方法就是通过成员函数。
- 如果类中所有public权限的成员都是函数,客户在访问类成员时只会默认访问函数,不需要考虑访问的成员需不需要添加(),这就省下了许多搔首弄耳的时间。
-
- 可细微划分访问控制
- 使用成员函数可使得我们对变量的控制处理更加精细。
- 如果我们让所有的成员变量为public,每个人都可以读写它。
- 如果我们设置为private,我们可以实现“不准访问”、“只读访问”、“读写访问”,甚至你可以写出“只写访问”。
代码示例:
class AccessLevels{ public: //对只读属性进行只读访问 int getReadOnly(){ return readOnly; } //对读写属性进行读写访问 void setReadWrite(int val){ readWrite = val; } int getReadWrite(){ return readWrite; } //对只写属性进行只写访问 void setWriteOnly(int val){ writeOnly = val; } private: int readOnly; //对外只读访问 int noAccess; //外部不可访问 int readWrite; //读写访问 int writeOnly; //只写访问 };
练习:
- 设计一个Person类,Person类具有name和age属性,提供初始化函数(Init),并提供对name和age的读写函数(set,get),但必须确保age的赋值在有效范围内(0-100),超出有效范围,则拒绝赋值,并提供方法输出姓名和年龄.(10分钟)
2.4 类|总结
- C++中可以使用struct、class来定义一个类
struct和class的区别
- struct的默认成员权限是public
- class的默认成员权限是private
代码示例:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/76df93fdaa0b4268a4a9d11bd859bec4~tplv-k3u1fbpfcp-zoom-1.image) ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/29bd226454384b36b8d2f0d4102d180d~tplv-k3u1fbpfcp-zoom-1.image)
- 上面代码中person对象、p指针的内存都是在函数的栈空间,自动分配和回收的
- 可以尝试反汇编struct和class,看看是否有其他区别
- 实际开发中,用class表示类比较多
三、 面向对象程序设计案例
1. 设计立方体类
- 设计立方体类(Cube),求出立方体的面积( 2ab + 2ac + 2bc )和体积( a b c),分别用全局函数和成员函数判断两个立方体是否相等
代码示例:
//立方体类 class Cub{ public: void setL(int l){ mL = l; } void setW(int w){ mW = w; } void setH(int h){ mH = h; } int getL(){ return mL; } int getW(){ return mW; } int getH(){ return mH; } //立方体面积 int caculateS(){ return (*mLmW *+ mLmH + mWmH) 2; } //立方体体积 int caculateV(){ return mL * mW * mH; } //成员方法 bool CubCompare(Cub& c){ if (getL() == c.getL() && getW() == c.getW() && getH() == c.getH()){ return true; } return false; } private: int mL; //长 int mW; //宽 int mH; //高 }; //比较两个立方体是否相等 bool CubCompare(Cub& c1, Cub& c2){ if (c1.getL() == c2.getL() && c1.getW() == c2.getW() && c1.getH() == c2.getH()){ return true; } return false; } void test(){ Cubc1, c2; c1.setL(10); c1.setW(20); c1.setH(30); c2.setL(20); c2.setW(20); c2.setH(30); cout<< "c1面积:" << c1.caculateS() << " 体积:" << c1.caculateV() << endl; cout<< "c2面积:" << c2.caculateS() << " 体积:" << c2.caculateV() << endl; //比较两个立方体是否相等 if (CubCompare(c1, c2)){ cout<< "c1和c2相等!" << endl; } else { cout<< "c1和c2不相等!" << endl; } if (c1.CubCompare(c2)){ cout<< "c1和c2相等!" << endl; } else { cout<< "c1和c2不相等!" << endl; } }
2. 点和圆的关系
- 设计一个圆形类(AdvCircle),和一个点类(Point),计算点和圆的关系。
假如圆心坐标为x0, y0, 半径为r,点的坐标为x1, y1:
- 1) 点在圆上:
(x1-x0)(x1-x0) + (y1-y0)(y1-y0) == r*r
- 2)点在圆内:
(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) < r*r
- 3)点在圆外:
(x1-x0)*(x1-x0) + (y1-y0)*(y1-y0) > r*r
- 1) 点在圆上:
- 代码示例:
//点类
class Point{
public:
void setX(int x){ mX = x; }
void setY(int y){ mY = y; }
int getX(){ return mX; }
int getY(){ return mY; }
private:
int mX;
int mY;
};
//圆类
class Circle{
public:
void setP(int x,int y) {
mP.setX(x);
mP.setY(y);
}
void setR(int r){ mR = r; }
Point& getP(){ return mP; }
int getR(){ return mR; }
//判断点和圆的关系
void IsPointInCircle(Point& point) {
int distance = (point.getX() - mP.getX()) * *(point.getX() - mP.getX()) + (point.getY() - mP.getY()) (point.getY() - mP.getY());
int radius = mR * mR;
if (distance < radius){
cout<< "Point(" << point.getX() << "," << point.getY() << ")在圆内!" << endl;
} else if (distance > radius){
cout<< "Point(" << point.getX() << "," << point.getY() << ")在圆外!" << endl;
} else {
cout<< "Point(" << point.getX() << "," << point.getY() << ")在圆上!" << endl;
}
}
private:
PointmP; //圆心
int mR; //半径
};
void test(){
//实例化圆对象
Circlecircle;
circle.setP(20, 20);
circle.setR(5);
//实例化点对象
Pointpoint;
point.setX(25);
point.setY(20);
circle.IsPointInCircle(point);
}
四、对象的构造和析构
1. 初始化和清理
初始设置
- 我们大家在购买一台电脑或者手机,或者其他的产品,这些产品都有一个初始设置
- 也就是这些产品对被创建的时候会有一个基础属性值
删除信息数据
- 那么随着我们使用手机和电脑的时间越来越久,那么电脑和手机会慢慢被我们手动创建很多文件数据
- 某一天我们不用手机或电脑了,那么我们应该将电脑或手机中我们增加的数据删除掉,保护自己的信息数据
初始化和清理
- 从这样的过程中,我们体会一下,所有的事物在起初的时候都应该有个初始状态,当这个事物完成其使命时,应该及时清除外界作用于上面的一些信息数据
那么我们C++中OO思想也是来源于现实,是对现实事物的抽象模拟。具体来说:
- 当我们创建对象的时候,这个对象应该有一个初始状态
- 当对象销毁之前应该销毁自己创建的一些数据
对象的初始化和清理也是两个非常重要的安全问题
- 一个对象或者变量没有初始时,对其使用后果是未知
- 同样的使用完一个变量,没有及时清理,也会造成一定的安全问题
C++为了给我们提供这种问题的解决方案:
构造函数
和析构函数
- 这两个函数将会被编译器自动调用,完成对象
初始化
和对象清理
工作
无论你是否喜欢,对象的初始化和清理工作是编译器强制我们要做的事情
- 即使你不提供初始化操作和清理操作,编译器也会给你增加默认的操作
- 这个默认初始化操作不会做任何事,所以编写类就应该顺便提供初始化函数
为什么初始化操作是自动调用而不是手动调用?
- 既然是必须操作,那么自动调用会更好
- 如果靠程序员自觉,那么就会存在遗漏初始化的情况出现
2. 构造函数和析构函数
2.1 构造函数
- 构造函数(也叫构造器),在对象创建的时候自动调用,一般用于完成对象的初始化工作
- 构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用
构造函数语法:
- 构造函数函数名和类名相同,没有返回值,不能有void,但可以有参数。
- ClassName(){}
特点
- 函数名与类同名,无返回值(void都不能写),可以有参数,可以重载,可以有多个构造函数
- 一旦自定义了构造函数,必须用其中一个自定义的构造函数来初始化对象
注意
- 通过malloc分配的对象不会调用构造函数
一个广为流传的、很多教程\书籍都推崇的错误结论:
- 默认情况下,编译器会为每一个类生成空的无参的构造函数
- 正确理解:在某些特定的情况下,编译器才会为类生成空的无参的构造函数
- (哪些特定的情况?以后再提)
- 构造函数的调用
默认情况下,成员变量的初始化
- 如果自定义了构造函数,除了全局区,其他内存空间的成员变量默认都不会被初始化,需要开发人员手动初始化
成员变量的初始化
- 对象初始化
2.2 析构函数
- 析构函数(也叫析构器),在对象销毁的时候自动调用,一般用于完成对象的清理工作
- 析构函数主要用于对象
销毁前
系统自动调用,执行一些清理工作。 特点
- 函数名以~开头,与类同名,无返回值(void都不能写),无参,不可以重载,有且只有一个析构函数
析构函数语法:
- 析构函数函数名是在类名前面加”~”组成,没有返回值,不能有void,不能有参数,不能重载。
- ~ClassName(){}
注意
- 通过malloc分配的对象free的时候不会调用析构函数
构造函数、析构函数要声明为
public
,才能被外界正常使用- 对象内部申请的堆空间,由对象内部回收
2.3 代码示例
class Person{
public:
Person(){
cout << "构造函数调用!" << endl;
pName = (*char)*malloc(sizeof("John"));
strcpy(pName, "John");
mTall = 150;
mMoney = 100;
}
~Person(){
cout << "析构函数调用!" << endl;
if (pName != NULL){
free(pName);
pName = NULL;
}
}
public:
char pName;
int mTall;
int mMoney;
};
void test(){
Person person;
cout << person.pName << person.mTall << person.mMoney << endl;
}
3. 构造函数的分类及调用
按参数类型:
- 分为无参构造函数和有参构造函数
按类型分类:
- 普通构造函数和拷贝构造函数(复制构造函数)
代码示例:
class Person{ public: Person(){ cout<< "no param constructor!" << endl; mAge= 0; } //有参构造函数 Person(**int age){** cout<< "1 param constructor!" << endl; mAge= age; } //拷贝构造函数(复制构造函数) 使用另一个对象初始化本对象 Person(**const Person&** person){ cout<< "copy constructor!" << endl; mAge= person.**mAge;** } //打印年龄 void PrintPerson(){ cout<< "Age:" << mAge << endl; } private: int mAge; }; //1. 无参构造调用方式 void test01(){ //调用无参构造函数 Personperson1; person1.**PrintPerson();** //无参构造函数错误调用方式 //Person person2(); //person2.PrintPerson(); } //2. 调用有参构造函数 void test02(){ //第一种 括号法,最常用 Personperson01(**100);** person01.**PrintPerson();** //调用拷贝构造函数 Personperson02(**person01);** person02.**PrintPerson();** //第二种 匿名对象(显示调用构造函数) Person(**200);** //匿名对象,没有名字的对象 Personperson03 = Person(**300);** person03.**PrintPerson();** //注意: 使用匿名对象初始化判断调用哪一个构造函数,要看匿名对象的参数类型 Personperson06(**Person(400));** //等价于 Person person06 = Person(400); person06.**PrintPerson();** //第三种 =号法 隐式转换 Personperson04 = 100; //Person person04 = Person(100) person04.**PrintPerson();** //调用拷贝构造 Personperson05 = person04; //Person person05 = Person(person04) person05.**PrintPerson();** }
b为A的实例化对象,A a = A(b) 和 A(b)的区别?
- 当A(b) 有变量来接的时候,那么编译器认为他是一个匿名对象,当没有变量来接的时候,编译器认为你A(b) 等价于 A b.
注意: 不能调用拷贝构造函数去初始化匿名对象,也就是说以下代码不正确:
class Teacher{ public: Teacher(){ cout<< "默认构造函数!" << endl; } Teacher(const Teacher& teacher) { cout<< "拷贝构造函数!" << endl; } public: int mAge; }; void test(){ Teachert1; //error C2086:“Teacher t1”: 重定义 Teacher(t1); //此时等价于 Teacher t1; }
4. 声明和实现
- 声明和实现分离:\
5. 拷贝构造函数的调用时机
- 对象以值传递的方式传给函数参数
- 函数局部对象以值传递的方式从函数返回(vsdebug模式下调用一次拷贝构造,qt不调用任何构造)
用一个对象初始化另一个对象
class Person{ public: Person(){ cout<< "no paramcontructor!" << endl; mAge= 10; } Person(int age){ cout<< "param constructor!" << endl; mAge= age; } Person(const Person& person){ cout<< "copy constructor!" << endl; mAge= person.mAge; } ~Person(){ cout<< "destructor!" << endl; } public: int mAge; }; //1. 旧对象初始化新对象 void test01(){ Personp(10); Personp1(p); Personp2 = Person(p); Personp3 = p; // 相当于Person p2 = Person(p); } //2. 传递的参数是普通对象,函数参数也是普通对象,传递将会调用拷贝构造 void doBussiness(Person p){} void test02(){ Personp(10); doBussiness(p); } //3. 函数返回局部对象 Person MyBusiness(){ Personp(10); cout<< "局部p:" << (int*)&p << endl; return p; } void test03(){ //vs release、qt下没有调用拷贝构造函数 //vs debug下调用一次拷贝构造函数 Personp = MyBusiness(); cout<< "局部p:" << (int*)&p << endl; }
Test03结果说明:
- 编译器存在一种对返回值的优化技术,RVO(Return Value Optimization).在vs debug模式下并没有进行这种优化,所以函数MyBusiness中创建p对象,调用了一次构造函数,当编译器发现你要返回这个局部的对象时,编译器通过调用拷贝构造生成一个临时Person对象返回,然后调用p的析构函数
- 我们从常理来分析的话,这个匿名对象和这个局部的p对象是相同的两个对象,那么如果能直接返回p对象,就会省去一个拷贝构造和一个析构函数的开销,在程序中一个对象的拷贝也是非常耗时的,如果减少这种拷贝和析构的次数,那么从另一个角度来说,也是编译器对程序执行效率上进行了优化
所以在这里,编译器偷偷帮我们做了一层优化:
- 当我们这样去调用: Person p = MyBusiness();
- 编译器偷偷将我们的代码更改为:
void MyBussiness(Person& _result){ _result.X:X(); //调用Person默认拷贝构造函数 //.....对_result进行处理 return; } int main(){ Person p; //这里只分配空间,不初始化 MyBussiness(p); }
6. 构造函数调用规则
默认情况下,C++编译器至少为我们写的类增加3个函数
- 1.默认构造函数(无参,函数体为空)
- 2.默认析构函数(无参,函数体为空)
- 3.默认拷贝构造函数,对类中非静态成员属性简单值拷贝
- 如果用户定义拷贝构造函数,C++不会再提供任何默认构造函数
- 如果用户定义了普通构造(非拷贝),C++不再提供默认无参构造,但是会提供默认拷贝构造
7.深拷贝和浅拷贝
7.1 浅拷贝
- 同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝
- 一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。
7.2 深拷贝
- 当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。
代码示例:
class Person{ public: Person(*char name*,int age){ pName = (char)malloc(strlen(name) + 1); strcpy(pName,name); mAge = age; } //增加拷贝构造函数 Person(const Person& person){ pName = (*char)*malloc(strlen(person.pName) + 1); strcpy(pName, person.pName); mAge = person.mAge; } ~Person(){ if (pName != NULL){ free(pName); } } private: char pName; int mAge; }; void test(){ Person p1("Edward",30); //用对象p1初始化对象p2,调用c++提供的默认拷贝构造函数 Person p2 = p1; }
8. 多个对象构造和析构
8.1 初始化列表
- 构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。
初始化列表简单使用:
class Person{ public: #if 0 //传统方式初始化 Person(int a,int b,int c) { mA= a; mB= b; mC= c; } #endif //初始化列表方式初始化 Person(int a, int b, int c):mA(a),mB(b),mC(c){} void PrintPerson() { cout<< "mA:" << mA << endl; cout<< "mB:" << mB << endl; cout<< "mC:" << mC << endl; } private: int mA; int mB; int mC; };
注意
:初始化成员列表(参数列表)只能在构造函数使用。
8.2 类对象作为成员
类中的成员
- 在类中定义的数据成员一般都是基本的数据类型
- 但是类中的成员也可以是对象,叫做对象成员。
对象的初始化
- C++中对对象的初始化是非常重要的操作,当创建一个对象的时候,C++编译器必须确保调用了所有子对象的构造函数
- 如果所有的子对象有默认构造函数,编译器可以自动调用他们
- 但是如果子对象没有默认的构造函数,或者想指定调用某个构造函数怎么办?
那么是否可以在类的构造函数直接调用子类的属性完成初始化呢?
- 但是如果子类的成员属性是私有的,我们是没有办法访问并完成初始化的。
解决办法非常简单:
- 对于子类调用构造函数,C++为此提供了专门的语法,即
构造函数初始化列表
当调用构造函数时
- 首先按各对象成员在类
定义中的顺序(和参数列表的顺序无关
)依次调用它们的构造函数,对这些对象初始化 - 最后再调用本身的函数体
- 也就是说,
先调用对象成员的构造函数,再调用本身的构造函数
- 首先按各对象成员在类
- 对于子类调用构造函数,C++为此提供了专门的语法,即
对象的反初始化
- 析构函数和构造函数调用顺序相反,先构造,后析构。
代码示例:
//汽车类 class Car { public: Car(){ cout<< "Car 默认构造函数!" << endl; mName= "大众汽车"; } Car(string name){ cout<< "Car 带参数构造函数!" << endl; mName= name; } ~Car(){ cout<< "Car 析构函数!" << endl; } public: string mName; }; //拖拉机 class Tractor { public: Tractor(){ cout<< "Tractor 默认构造函数!" << endl; mName= "爬土坡专用拖拉机"; } Tractor(string name) { cout<< "Tractor 带参数构造函数!" << endl; mName= name; } ~Tractor() { cout<< "Tractor 析构函数!" << endl; } public: string mName; }; //人类 class Person{ public: #if 1 //类mCar不存在合适的构造函数 Person(string name){ mName= name; } #else //初始化列表可以指定调用构造函数 Person(string carName, string tracName, string name) : mTractor(tracName), mCar(carName), mName(name){ cout<< "Person 构造函数!" << endl; } #endif void GoWorkByCar(){ cout<< mName << "开着" << mCar.mName << "去上班!" << endl; } void GoWorkByTractor() { cout<< mName << "开着" << mTractor.mName << "去上班!" << endl; } ~Person(){ cout<< "Person 析构函数!" << endl; } private: string mName; Car mCar; Tractor mTractor; }; void test(){ //Person person("宝马", "东风拖拉机", "赵四"); Personperson("刘能"); person.GoWorkByCar(); person.GoWorkByTractor(); }
9. explicit关键字
C++提供了关键字explicit,禁止通过构造函数进行的隐式转换
- 声明为explicit的构造函数不能在隐式转换中使用。
explicit注意
- explicit用于修饰构造函数,防止隐式转化
- 是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言
代码示例:
class MyString{ public: explicit MyString(int n){ cout<< "MyString(int n)!" << endl; } MyString(*const char str*){ cout<< "MyString(const char str)" << endl; } }; int main(){ //给字符串赋值?还是初始化? //MyString str1 = 1; MyStringstr2(10); //寓意非常明确,给字符串赋值 MyStringstr3 = "abcd"; MyStringstr4("abcd"); return EXIT_SUCCESS; }
10. 动态对象创建
数组对象案例分析:
- 当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间
在使用数组的时,会有这样的问题
- 数组也许空间太大了,浪费空间,也许空间不足
- 所以对于数组来讲,如果能根据需要来分配空间大小再好不过
动态性问题
- 所以动态的意思意味着不确定性
- 为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求
当然
C
早就提供了动态内存分配(dynamic memory allocation)- 函数
malloc
和free
可以 在运行时从堆中分配存储单元
- 函数
- 然而这些函数在 C
++
中不能很好的运行,因为它不能帮我们完成对象的初始化工作。
内存
每个应用都有自己独立的内存空间,其内存空间一般都有以下几大区域
代码段(代码区)
- 用于存放代码
数据段(全局区)
- 用于存放全局变量等
栈空间
- 每调用一个函数就会给它分配一段连续的栈空间,等函数调用完毕后会自动回收这段栈空间
- 自动分配和回收
堆空间
- 需要主动去申请和释放
- 堆空间
- 在程序运行过程,为了能够自由控制内存的生命周期、大小,会经常使用堆空间的内存
堆空间的申请\释放
malloc \ free
new \ delete
new [] \ delete []
注意
- 申请堆空间成功后,会返回那一段内存空间的地址
- 申请和释放必须是1对1的关系,不然可能会存在内存泄露
现在的很多高级编程语言不需要开发人员去管理内存(比如Java),屏蔽了很多内存细节,利弊同时存在
- 利:提高开发效率,避免内存使用不当或泄露
- 弊:不利于开发人员了解本质,永远停留在API调用和表层语法糖,对性能优化无从下手
- 下图是X86环境(32bit)
- 堆空间的初始化
memset
- memset函数是将较大的数据结构(比如对象、数组等)内存清零的比较快的方法
对象的内存
- 对象的内存可以存在于3种地方
- 全局区(数据段):全局变量
- 栈空间:函数里面的局部变量
- 堆空间:动态申请内存(malloc、new等)
10.1 对象创建
当创建一个C++对象时会发生两件事:
-
- 为对象分配内存
-
- 调用构造函数来初始化那块内存
-
- 第一步我们能保证实现,需要我们确保第二步一定能发生
- C++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因
10.2 对象的内存布局
- 思考:如果类中有多个成员变量,对象的内存又是如何布局的?
10.3 C动态分配内存方法
C动态分配案例分析
- 为了在运行时动态分配内存,C在他的标准库中提供了一些函数:
malloc
以及它的变种calloc
和realloc
,释放内存的free
- 这些函数是有效的、但是原始的,需要程序员理解和小心使用
为了使用C的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:
class Person{ public: Person(){ mAge= 20; pName= (*char)*malloc(strlen("john")+1); strcpy(pName, "john"); } void Init(){ mAge= 20; pName= (char)malloc(strlen("john")+1); strcpy(pName, "john"); } void Clean(){ if (pName != NULL){ free(pName); } } public: int mAge; char* pName*; }; int main() { //分配内存 Person person = *(Person)malloc(sizeof(Person)); if(person == NULL){ return 0; } //调用初始化函数 person->Init(); //清理对象 person->Clean(); //释放person对象 free(person); return EXIT_SUCCESS; }
问题:
- 1) 程序员必须确定对象的长度
- 2) malloc返回一个void指针,C++不允许将void赋值给其他任何指针,必须强转
- 3) malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功
- 4) 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数
- C的动态内存分配函数太复杂,容易令人混淆,是不可接受的,C++中我们推荐使用运算符 new 和delete
10.4 new operator
- C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里
当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化
Person person = new Person; 相当于: Person person = (Person*)malloc(sizeof(Person)); if(person == NULL){ return 0; } person->Init();
- New操作符能确定在调用构造函数初始化之前内存分配是成功的,所有不用显式确定调用是否成功。
- 现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。
10.5 delete operator
new
表达式的反面是delete
表达式delete
表达式 先调用析构函数,然后释放内存- 正如
new
表达式返回一个指向对象的指针一样,delete
需要一个对象的地址 delete
只适用于由new
创建的对象- 如果使用一个由
malloc
或者calloc
或者realloc
创建的对象使用delete
,这个行为是未定义的 - 因为大多数
new
和delete
的实现机制都使用了malloc
和free
,所以很可能没有调用析构函数就释放了内存 如果正在删除的对象的指针是
NULL
,将不发生任何事- 因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次
- 对一些对象删除两次可能会产生某些问题
代码示例:
class Person { public: Person(){ cout << "无参构造函数!" << endl; pName = (char*)malloc(strlen("undefined") + 1); strcpy(pName, "undefined"); mAge = 0; } Person(char* name, int age){ cout << "有参构造函数!" << endl; pName= (char*)malloc(strlen(name) + 1); strcpy(pName, name); mAge = age; } void ShowPerson(){ cout << "Name:" << pName << "Age:" << mAge << endl; } ~Person(){ cout << "析构函数!" << endl; if (pName != NULL){ delete pName; pName = NULL; } } public: char* pName; int mAge; }; void test(){ Person* person1 = new Person; Person* person2 = new Person("John",33); person1->ShowPerson(); person2->ShowPerson(); delete person1; delete person2; }
10.6 用于数组的new和delete
- 使用new和delete在堆上创建数组非常容易
- 当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数
代码示例:
class Person{ public: Person(){ pName= (*char)*malloc(strlen("undefined") + 1); strcpy(pName, "undefined"); mAge= 0; } Person(char name, int age){ pName= (*char)*malloc(sizeof(name)); strcpy(pName, name); mAge= age; } ~Person(){ if (pName != NULL){ delete pName; } } public: char pName; int mAge; }; void test(){ //栈聚合初始化 Person person[] = { Person("john", 20), Person("Smith", 22) }; cout<< person[1].pName << endl; //创建堆上对象数组必须提供构造函数 Person* workers = new Person[20]; }
10.7 delete void*可能会出错
如果对一个
void*
指针执行delete
操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数.以下代码未调用析构函数,导致可用内存减少class Person{ public: Person(*char name*, int age){ pName= (char)malloc(sizeof(name)); strcpy(pName,name); mAge= age; } ~Person(){ if (pName != NULL){ delete pName; } } public: char* pName*; int mAge; }; void test(){ void person = new Person("john",20); delete person; }
- 问题:malloc、free和new、delete可以混搭使用吗?也就是说malloc分配的内存,可以调用delete吗?通过new创建的对象,可以调用free来释放吗?
10.8 使用new和delete采用相同形式
代码示例:
Person * person = new Person[10]; delete person;
- 以上代码有什么问题吗?(vs下直接中断、qt下析构函数调用一次)
- 使用了
new
也搭配使用了delete
,问题在于Person
有10个对象,那么其他9个对象可能没有调用析构函数,也就是说其他9个对象可能删除不完全,因为它们的析构函数没有被调用。 我们现在清楚使用
new
的时候发生了两件事:- 一、分配内存
- 二、调用构造函数
那么调用delete的时候也有两件事:
- 一、析构函数;
- 二、释放内存。
那么刚才我们那段代码最大的问题在于:
- person指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。
- 换句话说,person指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。
- 更明确的说,数组所用的内存通常还包括“数组大小记录”,使得delete的时候知道应该调用几次析构函数。
- 单一对象的话就没有这个记录。
- 单一对象和数组对象的内存布局可理解为下图:
- 本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的
- 当我们使用一个delete的时候,我们必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。当我们使用delete[],那么delete就知道是一个对象数组,从而清楚应该调用几次析构函数。
结论:
- 如果在new表达式中使用[],必须在相应的delete表达式中也使用[].如果在new表达式中不使用[], 一定不要在相应的delete表达式中使用[].
11. 静态成员
在类定义中,它的成员(包括成员变量和成员函数),这些成员可以用关键字static声明为静态的,称为静态成员。
不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享
11.1 静态成员变量
- 在一个类中,若将一个成员变量声明为static,这种成员称为静态成员变量。
- 与一般的数据成员不同,无论建立了多少个对象,都只有一个静态数据的拷贝。静态成员变量,属于某个类,所有对象共享
- 静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。
- 静态成员变量必须在类中声明,在类外定义
- 静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间
- 静态数据成员可以通过类名或者对象名来引用
代码示例:
class Person{ public: //类的静态成员属性 static int sNum; private: static int sOther; }; //类外初始化,初始化时不加static int Person::sNum = 0; int Person::sOther = 0; int main() { //1. 通过类名直接访问 Person::sNum = 100; cout<< "Person::sNum:" << Person::sNum << endl; //2. 通过对象访问 Personp1, p2; p1.sNum = 200; cout<< "p1.sNum:" << p1.sNum << endl; cout<< "p2.sNum:" << p2.sNum << endl; //3. 静态成员也有访问权限,类外不能访问私有成员 //cout <<"Person::sOther:" << Person::sOther << endl; Personp3; //cout <<"p3.sOther:" << p3.sOther << endl; system("pause"); return EXIT_SUCCESS; }
11.2 静态成员函数
- 在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数使用方式和静态变量一样,同样在对象没有创建前,即可通过类名调用。静态成员函数主要为了访问静态变量,但是,不能访问普通成员变量。
静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装
- 静态成员函数只能访问静态变量,不能访问普通成员变量
- 静态成员函数的使用和静态成员变量一样
- 静态成员函数也有访问权限
- 普通成员函数可访问静态成员变量、也可以访问非经常成员变量
- 代码示例:
class Person{
public:
//普通成员函数可以访问static和non-static成员属性
void changeParam1(int param){
mParam= param;
sNum= param;
}
//静态成员函数只能访问static成员属性
static void changeParam2(int param){
//mParam = param; //无法访问
sNum= param;
}
private:
static void changeParam3(int param){
//mParam = param; //无法访问
sNum= param;
}
public:
int mParam;
static int sNum;
};
//静态成员属性类外初始化
int Person::sNum = 0;
int main(){
//1. 类名直接调用
Person::changeParam2(100);
//2. 通过对象调用
Personp;
p.changeParam2(200);
//3. 静态成员函数也有访问权限
//Person::changeParam3(100);//类外无法访问私有静态成员函数
//Person p1;
//p1.changeParam3(200);
return EXIT_SUCCESS;
}
11.3 const静态成员属性
- 如果一个类的成员,既要实现共享,又要实现不可改变,那就用static const 修饰。定义静态const数据成员时,最好在类内部初始化。
代码示例:
class Person{ public: //static const int mShare =10; const static int mShare = 10; //只读区,不可修改 }; int main(){ cout<< Person::mShare << endl; //Person::mShare = 20; return EXIT_SUCCESS; }
11.4 静态成员实现单例模式
单例模式是一种常用的软件设计模式
- 在它的核心结构中只包含一个被称为单例的特殊类。
- 通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。
- 如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
Singleton(单例):
- 在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;
- 为了防止在外部对其实例化,将其默认构造函数和拷贝构造函数设计为私有;
- 在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
- 案例:\
用单例模式,模拟公司员工使用打印机场景,打印机可以打印员工要输出的内容,并且可以累积打印机使用次数。 代码:
class Printer{ public: static Printer* getInstance(){ return pPrinter;} void PrintText(string text){ cout<< "打印内容:" << text << endl; cout<< "已打印次数:" << mTimes << endl; cout<< "--------------" << endl; mTimes++; } private: Printer(){ mTimes = 0; } Printer(const Printer&){} private: static Printer* pPrinter; int mTimes; }; Printer* Printer::pPrinter = new Printer; void test(){ Printer* printer = Printer::getInstance(); printer->PrintText("离职报告!"); printer->PrintText("入职合同!"); printer->PrintText("提交代码!"); }
五、对象相关|大杂烩
1. 对象的内存布局
- 父类的成员变量在前,子类的成员变量在后
2. 初始化列表
特点
- 一种便捷的初始化成员变量的方式
- 只能用在构造函数中
- 初始化顺序只跟成员变量的声明顺序有关
- 图片中的2种写法是等价的\
思考
m_age、m_height的值是多少
- 10、170
m_age、m_height的值是多少
- 未知、180
初始化列表与默认参数配合使用
如果函数声明和实现是分离的
- 初始化列表只能写在函数的实现中
- 默认参数只能写在函数的声明中
3. 构造函数的互相调用
注意:下面的写法是错误的,初始化的是一个临时对象
![](https://ucc.alicdn.com/6tlhrlrn6deig_20230710_3b32b9d2dc7e42e0aa1bb4a40bbf3f63.png)
4. 父类的构造函数
- 子类的构造函数默认会调用父类的无参构造函数
- 如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数
- 如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数
- 继承体系下的构造函数示例
5. 构造、析构顺序
6. 拷贝构造函数(Copy Constructor)
- 拷贝构造函数是构造函数的一种
- 当利用已存在的对象创建一个新对象时(类似于拷贝),就会调用新对象的拷贝构造函数进行初始化
- 拷贝构造函数的格式是固定的,接收一个const引用作为参数
7. 调用父类的拷贝构造函数
拷贝构造函数
- car2、car3都是通过拷贝构造函数初始化的,car、car4是通过非拷贝构造函数初始化
- car4 = car3是一个赋值操作(默认是浅复制),并不会调用拷贝构造函数
8. 浅拷贝、深拷贝
编译器默认的提供的拷贝是浅拷贝(shallow copy)
- 将一个对象中所有成员变量的值拷贝到另一个对象
- 如果某个成员变量是个指针,只会拷贝指针中存储的地址值,并不会拷贝指针指向的内存空间
- 可能会导致堆空间多次free的问题
如果需要实现深拷贝(deep copy),就需要自定义拷贝构造函数
- 将指针类型的成员变量所指向的内存空间,拷贝到新的内存空间
- 深拷贝示例
9. 对象型参数和返回值
- 使用对象类型作为函数的参数或者返回值,可能会产生一些不必要的中间对象
10. 匿名对象(临时对象)
- 匿名对象:没有变量名、没有被指针指向的对象,用完后马上调用析构
11. 隐式构造
- C++中存在隐式构造的现象:某些情况下,会隐式调用单参数的构造函数
- 可以通过关键字explicit禁止掉隐式构造
12. 自动生成的构造函数
编译器自动生成的构造函数
C++的编译器在某些特定的情况下,会给类自动生成无参的构造函数,比如
- 成员变量在声明的同时进行了初始化
- 有定义虚函数
- 虚继承了其他类
- 包含了对象类型的成员,且这个成员有构造函数(编译器生成或自定义)
- 父类有构造函数(编译器生成或自定义)
总结一下
- 对象创建后,需要做一些额外操作时(比如内存操作、函数调用),编译器一般都会为其自动生成无参的构造函数
六、C++面向对象模型初探
1. 成员变量和函数的存储
C语言程序成员变量和函数的存储
- 在C语言中,成员变量和函数是“分开来声明的
- 也就是说,语言本身并没有支持“数据”和“函数”之间的关联性我们把这种程序方法称为“程序性的”:
- 由一组“分布在各个以功能为导航的函数中”的算法驱动,它们处理的是共同的外部数据
C++语言程序成员变量和函数的存储
- C++实现了“封装”
那么数据(成员属性)和操作(成员函数)是什么样的呢?
- “数据”和“处理数据的操作(函数)”是分开存储的。
- C++中的非静态数据成员直接内含在类对象中,就像C struct一样。
- 成员函数(member function)虽然内含在class声明之内,却不出现在对象中。
- 每一个非内联成员函数(non-inline member function)只会诞生一份函数实例
代码示例:
class MyClass01{ public: int mA; }; class MyClass02{ public: int mA; static int sB; }; class MyClass03{ public: void printMyClass(){ cout<< "hello world!" << endl; } public: int mA; static int sB; }; class MyClass04{ public: void printMyClass(){ cout<< "hello world!" << endl; } static void ShowMyClass(){ cout<< "hello world!" << endl; } public: int mA; static int sB; }; int main(){ MyClass01mclass01; MyClass02mclass02; MyClass03mclass03; MyClass04mclass04; cout<< "MyClass01:" << sizeof(mclass01) << endl; //4 //静态数据成员并不保存在类对象中 cout<< "MyClass02:" << sizeof(mclass02) << endl; //4 //非静态成员函数不保存在类对象中 cout<< "MyClass03:" << sizeof(mclass03) << endl; //4 //静态成员函数也不保存在类对象中 cout<< "MyClass04:" << sizeof(mclass04) << endl; //4 return EXIT_SUCCESS; }
- 通过上面的案例,我们可以的得出:C++类对象中的变量和函数是分开存储
2. this 指针
2.1 this指针工作原理
- 通过上例我们知道,C++的数据和操作也是分开存储,并且每一个非内联成员函数(non-inlinemember function)只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这一块代码是如何区分那个对象调用自己的呢?
- C++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象。
C++规定,this指针是隐含在对象成员函数内的一种指针。
- 当一个对象被创建后,它的每一个成员函数都含有一个系统自动生成的隐含指针this,用以保存这个对象的地址,也就是说虽然我们没有写上this指针,编译器在编译的时候也是会加上的。
- 因此this也称为“指向本对象的指针”,this指针并不是对象的一部分,不会影响sizeof(对象)的结果。
- this指针是C++实现封装的一种机制,它将对象和该对象调用的成员函数连接在一起,在外部看来,每一个对象都拥有自己的函数成员。
- 一般情况下,并不写this,而是让系统进行默认设置
- 成员函数通过this指针即可知道操作的是那个对象的数据。
this指针是一种隐含指针,它隐含于每个类的非静态成员函数中
。this指针无需定义,直接使用即可 - 注意: 静态成员函数内部没有this指针,静态成员函数不能操作非静态成员变量
2.2 this指针的使用
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
代码示例:
class Person{ public: //1. 当形参名和成员变量名一样时,this指针可用来区分 Person(string name,int age){ //name = name; //age = age; //输出错误 this->name = name; this->age = age; } //2. 返回对象本身的引用 //重载赋值操作符 //其实也是两个参数,其中隐藏了一个this指针 Person PersonPlusPerson(Person& person){ string newname = this->name + person.name; int newage = this->age + person.age; Person newperson(newname, newage); return newperson; } void ShowPerson(){ cout<< "Name:" << name << " Age:" << age << endl; } public: string name; int age; }; //3. 成员函数和全局函数(Perosn对象相加) Person PersonPlusPerson(Person& p1,Person& p2) { stringnewname = p1.name + p2.name; int newage = p1.age + p2.age; Personnewperson(newname,newage); return newperson; } int main(){ Personperson("John",100); person.ShowPerson(); cout<< "---------" << endl; Personperson1("John",20); Personperson2("001", 10); //1.全局函数实现两个对象相加 Personperson3 = PersonPlusPerson(person1, person2); person1.ShowPerson(); person2.ShowPerson(); person3.ShowPerson(); //2. 成员函数实现两个对象相加 Personperson4 = person1.PersonPlusPerson(person2); person4.ShowPerson(); system("pause"); return EXIT_SUCCESS; }
2.3 const修饰成员函数
- 用const修饰的成员函数时,const修饰this指针指向的内存区域,成员函数体内不可以修改本类中的任何普通成员变量,
- 当成员变量类型符前用mutable修饰时例外。
代码示例:
//const修饰成员函数 class Person{ public: Person(){ this->mAge = 0; this->mID = 0; } //在函数括号后面加上const,修饰成员变量不可修改,除了mutable变量 void sonmeOperate() const{ //this->mAge = 200; //mAge不可修改 this->mID = 10; } void ShowPerson(){ cout<< "ID:" << mID << " mAge:" << mAge << endl; } private: int mAge; mutable int mID; }; int main(){ Personperson; person.sonmeOperate(); person.ShowPerson(); system("pause"); return EXIT_SUCCESS; }
2.4 const修饰对象(常对象)
- 常对象只能调用const的成员函数
- 常对象可访问 const 或非 const 数据成员,不能修改,除非成员用mutable修饰
代码示例:
class Person{ public: Person(){ this->mAge = 0; this->mID = 0; } void ChangePerson() const{ mAge = 100; mID= 100; } void ShowPerson(){ this->mAge = 1000; cout<< "ID:" << this->mID << " Age:" << this->mAge << endl; } public: int mAge; mutable int mID; }; void test(){ const Person person; //1. 可访问数据成员 cout<< "Age:" << person.mAge << endl; //person.mAge = 300; //不可修改 person.mID = 1001; //但是可以修改mutable修饰的成员变量 //2. 只能访问const修饰的函数 //person.ShowPerson(); person.ChangePerson(); }
2.5 this指针|总结
- this是指向当前对象的指针
- 对象在调用成员函数的时候,会自动传入当前对象的内存地址
思考
可以利用this.m_age来访问成员变量么?
- 不可以,因为this是指针,必须用this->m_age
2.6 指针访问的本质
- 思考:最后打印出来的每个成员变量值是多少?
答案:
- 10 40 50
- 思考 如果将
person.display()
换成p->display()
呢?