文章目录
1、类和对象的基本概念
1.1C和C++中struct的区别
- C语言struct只有变量
- C++语言struct既有变量,也有函数
1.2类的封装
现实世界的事物所具有的共性就是每个事物都具有自身的属性,一些自身具有的行为;所以如果我们能把事物的属性和行为全都表示出来,也就可以抽象出来这个事物。
对象封装特性包含两个方面:一是属性和行为合成一个整体;二是给属性和行为增加访问权限。
1.2.1封装
- 1.把变量(属性)和函数(操作)合成一个整体,封装在一个类中
- 2.对变量和函数进行访问权限控制
1.2.2访问权限
- 1.在类的内部(作用域范围内),没有访问权限之分,所有成员可以相互访问
- 2.在类的外部(作用域范围外),访问权限才有意义:public private protected
- 3.在类的外部,只有public修饰的成员才能被访问,在没有涉及继承和派生时,private和protected是同等级的,外部不允许访问
struct和class的区别?
class默认访问权限为private,struct默认访问权限为public。
1.3将成员变量设置为private
- 1.可赋予客户端访问数据的一致性(不需要考虑访问的成员是否需要加括号)
- 2.可细微划分访问权限控制
2、面向对象程序涉及案例
2.1设计立方体类
class Cub{
private:
int L;
int W;
int H;
public:
void setL(int l){L = l;}
void setW(int w){W = w;}
void setH(int h){H = h;}
int getL(){return L;}
int getW(){return W;}
int getH(){return H;}
//计算立方体的面积
//计算立方体的体积
}
2.2点和圆的关系
3、对象的构造和析构
3.1初始化和清理
当我们创建对象时,这个对象应该由一个初始状态,当对象销毁之前应该销毁自己创建的一些数据。构造函数和析构函数,会被编译器自动调用,完成对象初始化和对象清理工作。
3.2构造函数和析构函数
构造函数主要作用在于创建对象时为对象成员的属性赋值,构造函数由编译器自动调用,无需手动调用。
析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法:
1.构造函数函数名和类名相同,没有返回值,不能有void,但可以有参数;
2.classname(){}
析构函数语法:
1.析构函数函数名是在类名前面加~,没有返回值,不能有void,不能有参数,不能重载;
2.~classname(){}
3.3构造函数的分类及调用
构造函数和析构函数权限访问类型必须是public。
按参数类型:无参构造(默认构造)、有参构造
按类型:普通构造函数、拷贝构造函数(复制构造函数)
Person(const Person& p){ //引用传递一个Person对象,且不能被修改,完全复制。
cout << "拷贝构造函数!"<< endl;
m_age = p.m_age;
}
调用有参构造函数
- 括号法
Person p1(18); //定义一个对象p1,并给其年龄赋值为18
//拷贝构造函数也属于有参构造函数
Person p2(p1);//将对象p1的属性和操作全部复制给p2,Person p2 = p1
- 匿名对象(显式调用构造函数)
Person(18);
Person p3 = Person(18);
//匿名对象拷贝构造
person p4(Person(18));//等价于Person p4 = Person(18)
- =号法,隐式转换
Person p5 = 18; // Person p5 = Person(18);
//调用拷贝构造
Person p6 = p5; //等价于Person p6 = Person(p5);
Person p1 = Person(p2) 和 Person(p2)的区别?
当Person(p2)有变量(对象)来接的时候,那么编译器认为它是一个匿名对象;当没有变量来接的时候,编译器认为Person(p2)等价于Person p2。
注意:不能调用拷贝构造函数去初始化匿名对象。
Person p1;
Person(p1);//用拷贝构造的方式去初始化匿名对象了,等价于Person p1,重复定义。
3.4拷贝构造函数的调用时机
- 1.用一个旧对象初始化新对象
三种方式:括号法,等号法,匿名对象。 - 2.对象以值传递的方式传递给函数参数
传递的参数是普通对象,函数参数也是普通对象,传递将会调用拷贝构造。 - 函数局部对象以值传递的方式从函数返回
函数返回局部对象。return p;
3.5构造函数调用规则
默认情况下,C++编译器至少为我们写的类增加3个函数
1.默认构造函数(无参构造函数,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对类中非静态成员属性简单值拷贝
如果用户定义拷贝构造函数,编译器将不会再提供任何默认构造函数。
如果用户定义了普通构造函数(非拷贝构造函数),编译器不再提供默认无参构造,但会提供默认拷贝构造函数。
3.6深拷贝和浅拷贝
3.6.1浅拷贝
C++的默认拷贝构造函数就是浅拷贝,是对象数据成员的简单复制,但是两个对象共用同一个地址。一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间时,当对象快结束时由于会两次调用析构函数,从而导致第二次析构时内存释放失败。
3.6.2深拷贝
需要自定义拷贝构造函数,自行给指针动态分配内存空间,仅仅把一个对象的数据复制过来,但是二者指向堆区不同的内存空间,两次析构时释放的是两个不同的内存空间。
3.7多个对象构造和析构
3.7.1初始化列表
//传统方式初始化
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){}
注意:初始化成员列表(参数列表)只能在构造函数使用
3.7.2类对象作为成员
当调用构造函数时,首先按照各对象成员在类中定义的顺序依次调用它们的构造函数,对这些对象依次初始化,然后再调用本身的函数体。也就是说,先调用对象成员的构造函数,再调用本身的构造函数。
析构函数和构造函数调用顺序相反,先构造的,后析构。
3.8 explicit关键字
声明为explicit的构造函数不能在隐式转换中使用,即不能将构造函数的参数列表内容直接赋值给对象。
explicit用于修饰构造函数,防止隐式转换;
是针对单参数的构造函数而言。(或者除了第一个参数外其余参数都有默认值的多参构造)
3.9动态对象创建
C语言提供了动态内存分配,函数malloc和free可以在运行时从堆中分配存储单元。
3.9.1对象创建
当创建一个C++对象时会发生两件事情:
1.为对象分配内存
2.调用构造函数来初始化那块内存
使用未初始化的对象是程序出错的一个重要原因。
3.9.2 C语言动态分配内存方法
- 程序员必须确定对象的长度;
- malloc返回一个void*的指针, C++不允许将void*赋值给其他任何指针,必须强转;
- malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功;
- 用户在使用对象之前必须对它初始化,构造函数不能显式调用初始化,用户有可能忘记调用初始化函数。
3.9.3 new 运算符
在C++中,把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。
new运算符能确定在调用构造函数初始化之前内存分配时成功的,不需要显式确定是否调用成功。
3.9.4 delete 运算符
delete只适用于由new创建的对象。delete表达式先调用析构函数,然后释放内存,正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。
3.9.5 用于数组的new和delete
使用new在堆上创建数组非常容易,如int* arr1 = new int[10]。释放数组内存为delete [ ] arr1.[ ]不能省略。
当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数。除了在栈上可以聚合初始化,创建堆上对象数组必须提供构造函数。
3.9.6 delete void*可能会出错
3.9.7 使用new 和delete采用相同形式
当我们使用delete的时候,必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是告诉它。当用delete [ ] 时,delete就知道是一个对象数组,从而清楚该调用几次析构函数。
如果在new表达式中使用[ ],必须在相应的delete表达式中也使用[ ];如果new表达式没有使用,也一定不要在相应的delete表达式中使用。
3.10 静态成员
类中的成员如果用关键字static修饰,则为静态成员。
不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享。
3.10.1静态成员变量
静态成员变量,在编译阶段就分配空间,对象还没创建时,就已经分配空间。
静态成员变量必须在类中声明,在类外定义。
静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间。
静态数据成员可以通过类名或者对象名来引用。
静态成员也有访问权限,类外不能访问私有成员。
3.10.2 静态成员函数
在成员函数前加static成为静态成员函数。和静态成员一样,在对象还没有创建前,可以通过类名调用。静态成员函数主要是为了访问静态变量,但不能访问普通成员变量。
- 静态成员函数只能访问静态成员变量,不能访问普通成员变量;
- 静态成员函数的使用和静态成员变量一样;
- 静态成员函数也有访问权限;
- 普通成员函数可以访问静态成员变量,也可以访问普通成员变量。
3.10.3 const静态成员属性
如果一个类的成员既要实现共享,又要实现不可改变,则用static const 修饰。定义静态const数据成员时,最好在类内部初始化。
3.10.4 静态成员实现单例模式
单例模式是一种常见的设计模式。
它的核心结构中只包含一个被称为单例的特殊类。
通过单例模式可以保证系统一个类只有一个实例而且该实例易于外界访问。
如果在系统中某个类的对象只能存在一个,单例模式就是最好的解决方案。
4、C++面向对象模型初探
4.1 成员变量和函数的存储
C++实现了封装,但是数据和处理数据的操作(函数)是分开存储的。
C++中的非静态成员变量直接内含在类对象中,就像C struct一样。
静态成员变量并不保存在类对象中。
成员函数(包括静态和非静态)虽然内含在class声明之内,却不出现在对象中。
每一个非内联成员函数只会诞生一份函数实例。
C++类对象中的变量和函数是分开存储的。
4.2 this 指针
4.2.1 this 指针工作原理
C++的数据和操作是分开存储的,并且每一个非内联成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。 那么,这一块代码是如何区分哪个对象调用自己呢?this 指针指向被调用的成员函数所属的对象。
- C++规定,this指针是隐含在对象成员函数内的一种指针。
- 当一个对象被创建后,它的每一个成员函数都含有一个系统自动生成的隐含指针this,用以保存这个对象的地址。即使我们没有写上this指针,编译器编译时也会自动加上。this指针又被称为“指向本对象的指针”,this 指针并不是对象的一部分,不会影响sizeof(对象)的结果。
- this 指针是C++实现封装的一种机制,它将对象和该对象调用的成员函数连接在一起,这样在外部看来就是,每一个对象都拥有属于自己的函数成员,但实际上每一个非内联的成员函数只有一份。
this 指针永远指向当前对象。
注意:静态成员函数内部没有this指针,this指针是一种隐含指针,它隐含于每个类的非静态成员函数中。静态成员函数不能操作非静态成员变量。
4.2.2 this 指针的使用
- 当形参和成员变量同名时,可用this 指针来区分。
- 在类的非静态成员函数中返回对象本身,可使用return *this。
4.2.3 const修饰成员函数
用const修饰的成员函数时,const修饰this 指针指向的内存区域,成员函数体内不可以修改本类中的任何普通成员变量;
当成员变量类型符前用mutable修饰时例外。
4.2.4 const修饰对象(常对象)
- 常对象只能调用const的成员函数
- 常对象可访问const或非const数据成员,不能修改,除非成员用mutable修饰。
5、友元
类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部访问。但是当有时需要在类的外部访问类的私有成员时,需要用到友元函数。
5.1 友元语法
- friend 关键字只出现在函数声明处
- 其他类、类成员函数、全局函数都可声明为友元
- 友元函数不是类的成员,不带this指针
- 友元函数可访问对象任意成员属性,包括私有属性
友元类注意:
- 1.友元关系不能被继承
- 2.友元关系是单向的,类A是类B的朋友,但类B不一定是类A的朋友
- 3.友元关系不具有传递性,类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友
5.2 课堂练习
让remote类作为televison类的友元类,就可以访问其内的私有元素。
强化训练(数组类封装)
6、运算符重载
6.1 运算符重载基本概念
对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
运算符重载(operator overloading)只是一种语法上的方便,也就是它只是另一种函数调用的方式。
在C++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样,也是一个函数,当编译器遇到适当的模式时,就会自动调用这个函数。
语法:
定义重载运算符就像定义函数,只是该函数的名字是operator#,#代表不同的被重载的运算符。函数中参数个数取决于两个因素:
1.运算符是一元(一个参数)的还是二元(两个参数);
2.运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数)。
-----对于内置的数据类型(如整型)的表示总的所有运算符是不能改变的,比如两个整型数据相加的加法运算符不能被重载成其他形式。内置的数据类型的运算规则是基础。-----
6.2 运算符重载碰上友元函数
如果通过全局函数的方式来重载运算符,可能会需要访问到某些私有成员属性,因此通常将重载的函数前加一个friend,使其称为友元函数。
6.3 可重载的运算符
几乎C中所有的运算符都可以重载,但也有一些限制,比如不能改变运算符的优先级,不能改变运算符的参数个数等。
6.4 自增自减(++/–)运算符重载
规定:++a,编译器调用operator++(a);a++,编译器调用operator++(a,int)。对于–运算符同理。
调用代码的时候,要优先使用前置形式。除非确实需要后置形式返回的原值,前置和后置在语义上是等价的,输入工作量也相当,只是前置效率会略高一点,由于前置形式少创建了一个临时对象。
6.5 指针运算符(*,->)重载
6.6 赋值运算符=重载
6.7 等于和不等于(== !=)运算符重载
6.8 函数调用符号()重载
6.9 不要重载&& 和||
6.10 符号重载总结
- =, [ ], ( ) 和->操作法只能通过成员函数进行重载
- << 和 >> 只能通过全局函数配合友元函数进行重载
- 不要重载&& 和||,因为无法实现短路规则
6.11 强化训练——字符串类封装
7、继承和派生
7.1 继承概述
7.1.1 为什么需要继承
使用继承可以复用已有的代码,减小代码的重复性。
7.1.2 继承基本概念
通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。
一类是从基类继承过来的,一类是自己增加的成员;
从基类继承过来的表现为共性,而新增的成员体现了个性。
7.1.3 派生类定义
三种继承方式:public,private,protected。
单继承:每个派生类只直接继承了一个基类的特征。
多继承:多个基类派生出了一个派生类的继承关系。
7.2 派生类的访问控制
派生类拥有基类中全部成员变量和成员函数(构造函数和析构函数除外),但是派生类中继承的成员不一定能直接访问,不同的继承方式会导致不同的访问权限。
7.3 继承中的构造和析构
7.3.1 继承中的对象模型
子类由父类成员叠加子类新成员而成。
7.3.2 对象构造和析构的调用原则
建房子和拆房子
子类构造函数的执行顺序:先父类后子类。
子类析构函数的执行顺序:先子类后父类。
注意:当父类构造函数有参数时,需要在子类初始化列表中显示调用父类构造函数。
7.4 继承中同名函数的处理方法
当子类成员和父类成员同名时,子类依然会将父类的同名成员继承下来,不过子类访问其成员时,默认访问子类的成员(就近原则),也可以通过作用域::进行同名成员区分来访问基类的同名成员。
任何时候重新定义基类中的一个重载函数,在新类中所有的其他版本将被自动隐藏。
7.5 非自动继承的函数
构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建;operator=也不能被继承。在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。
7.6 继承中的静态成员特性
静态成员和非静态成员的共同点:
- 它们都可以被继承到派生类中;
- 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏;
- 如果我们改变基类中一个函数的特征,函数值或者参数个数,所有使用该函数名的基类版本都会被隐藏。
7.7 多继承
7.7.1 多继承的概念
同时从多个类继承。可能会带来一些二义性的问题,需要显示指定调用。
7.7.2 菱形继承和虚继承
两个派生类继承同一个基类,而又有某个类同时继承这两个派生类,被称为菱形继承。对于可能产生的二义性,可以采用虚基类来解决。
7.7.3 虚继承实现原理
使用虚基类时,编译器会通过一个vbptr指向一张表,保存了当前虚指针相对于基类的首地址的偏移量。
当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象。C++标准中选择在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他初始化语句都不会调用。
8、多态
8.1 多态基本概念
按照字面意思就是多种形态。多态性提供接口与具体实现之间的一种隔离,根据不同的对象类型来执行不同的函数。
C++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,派生类和虚函数实现就是运行时多态。
静态多态和动态多态的区别在于函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态;如果函数的调用地址不能在编译期间确定,而需要在运行时才能决定,这就属于动态多态。
面向对象程序设计一个基本原则:开闭原则(对修改关闭,对扩展开放)。
动态多态满足条件:
父类中有虚函数,子类重写父类的虚函数,父类的指针或引用指向子类的对象。
重写:子类重新实现父类中的虚函数,必须返回值,函数名,参数一致才称为重写,注意与重载区别。
8.2 向上类型转换及问题
8.2.1 问题抛出
对象可以作为自己的类或者作为它的基类的对象来使用,还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。也就是说:父类引用(或指针)可以指向子类对象,通过父类指针(或引用)来操作子类对象。
8.2.2 问题解决思路
把函数体与函数调用相联系称为绑定。
- 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时不需要写;
- 如果一个一个函数在基类中声明为virtual,那么在所有派生类中它都是virtual的;
- 在派生类中virtual函数重定义称为重写;
- virtual关键字只能修饰成员函数;
- 构造函数不能为虚函数。
8.2.3 C++如何实现动态绑定
当编译器中有虚函数时编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,vpointer,vptr。这个指针vptr指向虚函数表,在多态调用的时候,根据vptr,找到虚函数表来实现动态绑定,如果子类中没有重写虚函数,vptr指向的基类的虚函数,如果重写了,vptr指向子类的虚函数。
多态的成立条件
- 有继承
- 子类重写父类虚函数。返回值,函数名字,函数参数,必须和父类一致;子类中virtual可写可以不写,建议写
- 类型兼容,父类指针,父类引用 指向子类对象
8.3 抽象基类和纯虚函数
- 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器会阻止这个操作;
- 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是抽象类;
- virtual void func() = 0;到素编译器在vtable中为函数保留一个位置,但这个特定位置不放地址。
8.4 纯虚函数和多继承
接口类中只有函数原型定义,没有任何数据定义。子类需要根据功能说明定义功能实现。除了析构函数外,其他声明都是纯虚函数。
8.5 虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。普通析构不会调用子类析构。
必须为纯虚析构函数提供一个函数体。
如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之,则应该为类声明虚析构函数。
8.6 重写 重载 重定义
- 重载
同一个作用域,同一个函数名,参数个数,参数顺序,参数类型不同,和返回值类型没有关系。
- 重定义(隐藏)
有继承,子类重新定义父类的同名成员(非virtual函数)
- 重写
有继承;子类重写父类的virtual函数;函数返回值,函数名,函数参数必须和基类中的虚函数一致。