基本语法
c++在创建变量时,必须给变量一个初始值,否则会报错。
switch语句中的表达式类型只能是整型或者字符型。
case中如果没有break,那么程序会一直向下执行。
二维数组的定义方式:
//第一种
int arr[2][3];
arr[0][0] = 1;
//第二种
int arr2[2][3] =
{
{1,2,3},
{4,5,6}
};
//第三种
int arr3[2][3] = { 1,2,3,4,5,6 };
指针
我们可以通过&符号获取变量的地址。
我们可以利用指针记录地址。
对指针变量解引用,可以操作指针指针指向的内存。
空指针:指针变量指向内存空间中编号为0的空间。
空指针用途:用于初始化指针变量。(空指针指向的内存是不可访问的,不可解引用)。
野指针:指针变量指向非法的内存空间。不可访问。
const修饰指针有三种情况。
-
const修饰指针 ——常量指针
//const修饰的是指针,指针指向可以改,指针指向的值不可以更改 const int * p1 = &a; p1 = &b; //正确 //const修饰的是常量,指针指向不可以改,指针指向的值可以更改 int * const p2 = &a; //p2 = &b; //错误 *p2 = 100; //正确 //const既修饰指针又修饰常量 const int * const p3 = &a; //p3 = &b; //错误 //*p3 = 100; //错误
-
const修饰常量——指针常量
-
const既修饰指针,又修饰常量
指针和数组
当指针指向一维数组时,指向的是数组的第一个元素,通过解引用即可获得第一个元素的值。
当数组名传入到函数作为参数时,被退化为指向首元素的指针。
结构体指针
通过指针访问结构体中的成员。
结构体中的const使用场景:加入const防止误操作,即不小心修改结构体的值。数组前加const也是同样的道理。
核心编程
内存分区编码
c++在运行前分为全局区和代码区
代码区的特点是共享和只读
全局区存放全局变量\静态变量\常量
常量区存放const修饰的全局常量和字符串常量
局部变量存放在栈区
利用new创建的数据,会返回该数据对应类型的指针
引用
给变量起个别名。
引用必须初始化,引用在初始化后,不可改变。
可以让引用做函数参数,可简化指针修改实参。
总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单
引用可以作为函数返回值。
不要返回局部变量引用。
引用的本质就是一个指针常量。
常量引用(const int& a),用于防止误操作。
函数提高
如果某个位置参数有默认值,那么从这个位置往后,必须都要有默认值。
如果函数申明有默认值,那么函数实现的时候就不能有默认参数。
c++的函数的形参列表中可以有占位参数,用来占位,调用函数时必须填补该位置。
函数重载条件:
-
同一个作用域下
-
函数名称相同
-
函数参数类型不同或者个数不同或者顺序不同
函数的返回值不可作为函数重载的条件
引用可以作为重载条件
类和对象
c++面向对象三大特性:封装、继承、多态
三种访问权限:
- 公共权限 public 类内可以访问 类外可以访问
- 保护权限 protected 类内可以访问 类外不可以访问
- 私有权限 private 类内可以访问 类外不可以访问
struct和class的区别:其唯一区别在于默认的访问权限不一样。(struct默认权限为公共,class的默认权限为私有)
一个对象或者变量没有初始状态,对其使用后果是未知的,同理,在使用完之后,没有及时清理,也会造成安全问题。
c++中利用了构造函数和析构函数来解决上述问题,,这两个函数都会被编译器自动调用。完成对象初始化和清理工作。
-
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
构造函数语法:
类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
-
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
析构函数语法:
~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
构造函数分类:
-
按照参数分类为有参构造和无参构造,无参又称为默认构造函数
-
按照类型分类为普通构造和拷贝构造
默认情况下,c++编译器至少给一个类添加三个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
-
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
-
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
深拷贝和浅拷贝:
- 浅拷贝:简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间,进行拷贝操作
初始化列表:用来初始化属性
- 语法:
构造函数():属性1(值1),属性2(值2)... {}
当类中成员是其他类对象时,我们称该成员为对象成员。构造的顺序是先调用对象成员的构造,再调用本类的构造。析构顺序与构造相反。
静态成员变量的两种访问方式:通过对象访问,通过类名访问。私有权限访问不到。
在c++中成员变量和成员函数是分开存储的。只有非静态成员变量才属于类的对象上。非静态成员变量占用对象空间;静态成员变量和函数不占对象空间。
c++提供this指针,指向被调用的成员函数所属的对象。
c++中的空指针也是可以调用成员函数的,但是也要注意有没有用到this指针。如果用到this指针就不可以。
成员函数后加const后我们称这个函数为常函数。常函数内不可更改成员属性。成员属性声明是加关键字mutable后,在常函数中依然可以修改。
声明对象前加const称该对象的常对象。常对象只能调用常函数。常对象不能修改成员变量的值,但是可以访问。常对象可以修改mutable修饰成员变量。
友元
目的:让一个函数或者类访问另一个类中的私有成员。
关键字:friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
运算符重载
对已有的运算符重新进行定义,赋予另一种功能,以适应不同的数据类型。
继承
可以减少重复的代码
class A : public B
A类称为子类或者派生类
B类称为父类或者基类
继承一共三种方式
-
公共继承
-
保护继承
-
私有继承
父类中私有成员也是被子类继承下去了,只是有编译器给隐藏后访问不到。
子类继承父类后,当创建子类对象,也会调用父类的构造函数。(先调用父类构造函数,再调用子类构造函数,析构顺序相反)。
当子类与父类出现同名的成员时,,如何通过子类对象,访问到子类或父类中同名的数据呢?
答:访问子类同名成员,直接访问即可;访问父类同名成员,需要加作用域。
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
多继承
语法:class 子类 :继承方式 父类1 , 继承方式 父类2...
多继承可能会引发父类中同名成员出现,需要加作用域区分。(c++实际开发中不建议用多继承)
菱形继承问题:
-
羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
-
草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
总结:
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义。
- 利用虚继承可以解决菱形继承问题。
多态
多态分为两类:
- 静态多态:函数重载 和 运算符重载属于静态多态, 复用函数名。
- 动态多态:派生类和虚函数实现运行时多态。
静态多态和动态多态区别:
- 静态多态的函数地址早绑定——编译阶段确定函数地址
- 动态多态的函数地址晚绑定——运行阶段确定函数地址
多态满足关系:
- 有继承关系
- 子类重写父类中的虚函数(函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。只有加了virtual关键字,子类重写才有意义)
多态使用:
- 父类指针或着引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称之为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
纯析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方式:将父类中的析构函数改为纯析构或者纯虚析构。
虚析构和纯虚析构的共性:
- 可以解决父类指针释放子类对象。
- 都需要有具体的函数实现
虚析构和 纯虚析构的区别:
- 如果是纯虚析构,则该类属于抽象类,无法实例化对象。
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
C++提高编程
模板
特点:
- 模板不可以直接使用,只是一个框架
- 模板的通用并不是万能的
两类模板机制:
-
函数模板
建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
template<typename T> 函数声明或定义
总结:
- 函数模板利用关键字template
- 使用函数模板有两种方式:自动类型推导、显示指定类型
- 模板的目的是为了提高复用性,将类型参数化。
注意事项
- 自动类型推导,必须推导出一致的数据类型T
- 模板必须确定出T的数据类型,才可以使用
普通函数与函数模板的区别:
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,则不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类中转换
利用具体化的模板,可以解决自定义类型的通用化
-
类模板
类模板作用:建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟类型来代表。
-
类模板和函数模板的区别:
类模板中没有自动类型推导的使用方式
类模板在模板参数列表中可以有默认参数
总结:
- 类模板使用只能用显示指定类型方式
- 类模板中的模板参数列表中可以有默认参数
类模板中成员函数创建时机:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数只有在使用时才创建
类模板函数对象做函数参数:
-
-
类模板与继承:
当类模板碰到继承时,需要注意一下几点:
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
STL初识
STL六大组件:
容器、算法、迭代器、仿函数、适配器、空间配置器
容器:
-
序列式容器
强调值的排序,序列容器中的每个元素均有固定的位置。
-
关联式容器
二叉树结构,各元素之间没有严格的物理上的顺序关系。
vector
vector<int>::iterator pBegin = v.begin();//指向vector中的第一个元素
vector<int>::iterator pEnd = v.end();//指向vector中的最后一个元素的下一位置。
while (pBegin!=pEnd) {
cout << *pBegin <<endl;
pBegin++;
}
=============================================
c++中引用传递和指针传递的区别。
引用其实就是变量的别名,操作的地址还是实参的地址,对引用操作就是对实参操作;
引用被创建的同时必须被初始化(指针可以在任何时候进行初始化)
不能有null引用,引用必须与存储的合法单元关联(指针则可以是null);
一旦引用被初始化,就不能改变引用的关系,(指针则可以随时改变所指的对象)
指针传递也是对实参进行操作;
指针传递参数本质上是值传递的方式,它所传递的是一个地址值;
深拷贝和浅拷贝需要注意的地方就是可变元素的拷贝:
在浅拷贝时,拷贝出来的新对象的地址和原对象是不一样的,但是新对象里面的可变元素(如列表)的地址和原对象里的可变元素的地址是相同的,也就是说浅拷贝它拷贝的是浅层次的数据结构(不可变元素),对象里的可变元素作为深层次的数据结构并没有被拷贝到新地址里面去,而是和原对象里的可变元素指向同一个地址,所以在新对象或原对象里对这个可变元素做修改时,两个对象是同时改变的,但是深拷贝不会这样,这个是浅拷贝相对于深拷贝最根本的区别。
堆栈的区别
栈:由os自动分配释放,存放函数的参数值,局部变量的值等。类似于数据结构的栈。
堆:由程序员分配释放,若不释放,结束时由os回收,类似于链表
速度上:栈是编译时分配空间,堆是动态分配,所以栈更快。
栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。
堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
new和delete 与 malloc和free
属性上:new、delete是c++的关键字,需要编译器支持,malloc和free是库函数,需要头文件支持
参数:new申请内存不需要指明大小,malloc需要
返回:new返回的是对象类型的指针,更安全,malloc返回void*,需要类型转换。
分配失败 :new抛异常,malloc返回null
重载:允许重载new、delete
内存区域:new从自由存储区动态分配内存空间,malloc从堆上
define和const区别
起作用阶段:#define是在编译的预处理阶段,而const是在编译、运行的时候起作用。
作用方式:const常量有数据类型,而宏常量没有
存储方式:#define只是进行展开,有多少使用,就替换多少次
重载和重写的区别
重载:是指同一可访问区内被声明的几个具有不同参数列表(参数类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数。
重写:指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同,要有virtual修饰。
说下c++中的malloc和free
关于malloc和free这两个函数,malloc的用法示例:int *p=(int *)malloc(2*sizeof(int)); 它表示在堆中开辟一块大小为2*sizeof(int)的一块内存空间,p指向这块内存空间的起始地址,malloc前面的(int*)表示这块空间用来存储int型数组。开辟了这块空间后,可以修改这个空间中的值,例如为*p,*(p+1)做赋值操作,如果再次使用malloc函数,例如再写一个 int *q=(int *)malloc(2*sizeof(int)); 此时开辟的以q为起始地址长度为2*sizeof(int)的空间是不会覆盖p所指向的空间的。但是,如果在int *q=(int *)malloc(2*sizeof(int));之前写一个free(p),那么,q所指向的空间则有可能覆盖p指向的空间。**所谓的free(p)操作,其实是修改p所指向的空间的标记值,让其可以被覆盖。**尽管执行了free(p),p仍就指向以前的起始地址,依旧可以对*p,*(p+1)赋值,并且可访问他们(例如输出)。
说下c++中的malloc和new的区别
多态作用机制
接口的多种不同实现方式即为多态,当发出一条命令时,不同对象收到同样的命令后所做出的动作是不同的。
机制:子类继承父类,对父类方法进行改写
类实现接口,对接口方法的实现
声明的总是父类类型或者接口,创建的是实际类型
虚函数(实现类的多态性)
定义虚函数是为了允许用基类的指针来调用子类的这个函数。
基类定义虚函数,子类重写,在派生类中对基类定义的虚函数进行重写时,需要声明该方法为虚函数。
基类中: virtual void A()
派生类中:void A()
注:c++调用虚函数比普通函数慢:普通地址在编译期间指定,单纯的寻址调用。虚函数调用,首先要找虚函数表,然后找偏移地址调用。
纯虚函数:
在基类中声明的纯虚函数,基类中无定义,在派生类中都要定义实现方式。
基类中:virtual void A()= 0
派生类:void A() {实现}
与虚函数的区别:含有纯虚函数的类称为抽象类,只含有虚函数的不能称为抽象类,虚函数可以直接使用或子类重载后使用,纯虚函数必须在子类实现才可以使用,纯虚函数在基类中只有声明没有定义,虚函数必须实现
继承
一个对象直接使用另一对象的属性及方法,C++支持多继承
三特性
封装、继承、多态
红黑树
每个结点要么是红的要么是黑的。
根结点是黑的。
每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。
如果一个结点是红的,那么它的两个儿子都是黑的。
对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。
正是红黑树的这5条性质,使一棵n个结点的红黑树始终保持了logn的高度,从而也就解释了上面所说的“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论成立
有序map和无序map
**有序map**,内部使用红黑树实现。
缺点: 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
适用处:对于那些有顺序要求的问题,用map会更高效一些
**无序map**,内部使用哈希表实现,查找速度很快,哈希表的简历比较耗费时间。
对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map
排序算法时间复杂度
stl的容器和普通的数组有什么区别?
创建方式:
创建数组时必须指定大小。
而容器不必指定,可动态改变其大小。
存储方式:
数组在内存空间上是连续存储的
而容器中顺序容器vector和deque是连续存储的,但list是链式存储的。
访问效率:
访问数组元素时可根据数组下标直接访问相应位置的元素;而容器中顺序容器vector和deque支持对元素的随机访问,但list不支持。
元素操作:
list支持咋容器中间位置插入或删除元素,而其他不支持。
STL:可不可以在边遍历vector边删除(erase)元素
段错误
段错误是指程序尝试访问一段不可访问的内存。
产生原因:
解引用空指针
访问不可访问的内存空间(如内核空间)
访问不存在的内存地址
试图写一个只读内存空间(如代码段)
栈溢出(函数递归调用)
使用未初始化的指针(定义时没有初始化或者已经回收)
内存分区
一般内存主要分为:代码区、常量区、静态区(全局区)、堆区、栈区。
代码区:存放程序的代码,即CPU执行的机器指令,且是只读的。
常量区:存放常量(程序在运行期间不能够被改变的量,例如:10,字符串常量“abcde”,数组的名字等)。
静态区(全局区):静态变量和全局变量的存储区域是一起的,一旦静态区的内存被分配,静态区的内存直到程序全部结束之后才会被释放。
堆区:由程序员调用malloc()函数来主动申请的,需要用free()函数来释放内存,若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏。
栈区:存放函数内的局部变量、形参和参数返回值。栈区之中的数据的作用范围过了之后,系统就会回收、自动管理栈区的内存(分配内存、回收内存),不需要开发人员来手动管理。
static关键字详解
- 该变量在全局数据区分配内存,在文件中共享
- 未经初始化的静态全局变量会被程序自动初始化为0
- 静态全局变量不能被其他文件所用(相对于全局变量)
- 其他文件中有定义相同名字的变量时不会发生冲突(相对于全局变量)
用static声明静态局部变量
当有时候希望函数的局部变量的值在函数调用结束后不消失而保留原值,即其所占用的存储单元不释放,在下一次该函数调用时,该变量保留上一次函数调用结束时的值。这时就应该指定该局部变量为静态局部变量。
对静态局部变量的说明:
- 静态局部变量在静态存储区分配存储单元,在程序的整个运行期间都不释放
- 在编译期间为静态局部变量赋初值,且只赋初值一次。
- 如果在定义静态局部变量时没有赋初值,那么在编译时自动赋初值0或null
- 静态局部变量的作用域为函数内。在其他函数中是不可见的。
什么情况下需要使用静态局部变量?
- 需要保留函数上一次调用结束时的值。
- 如果初始化后,变量只被引用而不需要改变其值,则用静态局部变量比较方便,以免每次调用都要赋值。
用static声明静态全局变量
如果在程序设计中希望某些全局变量只限于本文件使用,而不被其他文件引用,则可以再定义全局变量时加一个static声明。
用static修饰函数
函数本质上是全局的,因为一个函数要被另外的函数调用,但是,也可以指定函数只能被本文件调用,而不能被其他文件调用。
用static修饰的函数称为内部函数,它只能被本文件内的其他函数调用。
在定义函数时,如果函数首部冠以关键字extern,则表示此函数是外部函数,可供其他文件调用。
static作用
- 隐藏
- 保持变量内容的持久(即使函数运行结束,变量也不会被销毁)
- 默认初始化为0
宏定义和内联函数对两个数比较大小
- 当函数本身比较简单的时候,进出函数的开销大。此时用宏比较好
- 宏没有参数检查,只是简单的替换
改进:内联函数–>内联函数通常就是将它在程序中的每个调用点上“内联的”展开。从而消除了把max写成函数的额外执行开销
内联函数可减少cpu的系统开销,并且程序的整体速度将加快,但当内联函数很大时,会有相反的作用,因此一般比较小的函数才使用内联函数。
内联函数是C++的增强特性之一,用来降低程序的运行时间。当内联函数收到编译器的指示时,即可发生内联:编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段而非程序运行阶段。
虚函数定义
虚函数是一种在基类定义为virtual的函数,并在一个或多个派生类中再定义的函数。虚函数的特点是,只要定义了一个基类指针,就可以指向派生类的对象。(通过虚函数可以实现动态绑定)
虚函数表link
为什么要把析构函数定义为虚函数
new出来的子类son的对象,采用一个父类father的指针来接收,故在析构的时候,编译器因为只知道这个指针是父类的,所以只将父类部分的内存析构了,而不会析构子类的内存,就造成了内存泄漏。基类析构函数定义为虚函数时,在子类对象的首地址开始会有一块基类的虚函数表拷贝,在析构子类对象时会删除此虚函数表,所以此时会调用基类的析构函数,所以内存是安全的。
为什么构造函数不能是虚函数
因为虚函数都对应一个虚函数表,虚函数表是存在对象内存空间的,如果析构函数是虚的,就需要一个虚函数表来调用,但是类还没实例化,没有内存空间就没有虚函数表,这是一个死循环。
内联函数、构造函数和静态成员函数可以定义为虚函数吗?
内联函数是编译时展开函数体,所有此时就需要有尸体,而虚函数是运行时才有实体,所以内联函数不可以为虚函数。
静态成员函数是属于类的,不属于任何一个类的对象,可以通过作用域以及类的对象访问,本身就是一个实体,所以不能定义为虚函数。
正确区分重载、重写和隐藏
处于同一个类中的函数才会出现重载。处于父类和子类中的函数才会出现重写和隐藏。
重载:同一类中,函数名相同,但参数列表不同。
重写:父子类中,函数名相同,参数列表相同,且有virtual修饰。
隐藏:父子类中,函数名相同,参数列表相同,但没有virtual修饰;函数名相同,参数列表不同,无论有无virtual修饰都是隐藏。
子类析构时要调用父类的析构函数吗?(析构和构造的调用顺序)
析构函数调用次序是先派生类的析构后基类的析构,也就是说在基类的析构调用时,派生刘的信息已经全部销毁了。定义一个对象时先调用基类的构造函数,然后调用派生类的构造函数;析构的时候恰恰相反:先调用派生类的析构函数,然后调用基类的析构函数。
面向对象有哪些特点,如何体现?
- 继承:保留父类的属性,开扩新的东西。通过子类可以实现继承,子类继承父类的所有状态和行为,同时添加自身的状态和行为。
- 封装:类的私有化。将代码及处理数据绑定在一起的一种编程机制,该机制保证程序和数据不受外部干扰。
- 多态:允许将父类对象设置成为和一个和多个它的子对象相等的技术。包括重载和重写。重载为编译时多态,重写为运行时多态。
多态
定义:是对于不同对象接收相同消息时产生不同的动作。C++的多态性具体体现在运行和编译两个方面:在程序运行时的多态性通过继承和虚函数来体现;
多态实现的两种方式:父类指针指向子类对象 或 将一个基类的引用类型赋值为它的派生类实例。(重要:虚函数 + 指针或引用)
构造函数、复制构造函数、析构函数、赋值运算符不能被继承。
在程序编译时多态性体现在函数和运算符的重载上;
指针和引用的区别?
相同点:
- 都是地址的概念;
- 指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。
区别:
- 指针是一个实体,而引用仅是个别名;
- 引用使用时无需解引用(*),指针需要解引用;
- 引用只能在定义时被初始化一次,之后不可变;指针可变;
- 引用没有 const,指针有 const;
- 引用不能为空,指针可以为空;
- “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
- 指针和引用的自增(++)运算意义不一样;
- 从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。
const修饰的变量好#define有什么区别
#define在预处理阶段进行简单的替换,const在编译阶段使用。
#define不做类型检查,仅仅展开替换,const有数据类型,会执行类型检查。
#define不分配内存,仅仅展开替换,const会分配内存
#define不能调试,const可以调试
#define定义的常量在替换后运行过程中会不断地占用内存,而const定义常量存储在数据段,只有一份copy,效率更高。
define可以定义一些简单的函数,const不可以。
vector内部数据结构是什?List/Map/Queue
vector(向量):相当于数组,但其大小可以不预先指定,并且自动扩展。它可以像数组一样被操作,由于它的特性我们完全可以将vector 看作动态数组。在创建一个vector 后,它会自动在内存中分配一块连续的内存空间进行数据存储,初始的空间大小可以预先指定也可以由vector 默认指定,这个大小即capacity ()函数的返回值。当存储的数据超过分配的空间时vector 会重新分配一块内存块,但这样的分配是很耗时的,效率非常低。
deque(队列):它不像vector 把所有的对象保存在一块连续的内存块,而是采用多个连续的存储块,并且在一个映射结构中保存对这些块及其顺序的跟踪。向deque 两端添加或删除元素的开销很小,它不需要重新分配空间。
list(列表):是一个线性链表结构,它的数据由若干个节点构成,每一个节点都包括一个信息块(即实际存储的数据)、一个前驱指针和一个后驱指针。它无需分配指定的内存大小且可以任意伸缩,这是因为它存储在非连续的内存空间中,并且由指针将有序的元素链接起来。
set, multiset, map, multimap 是一种非线性的树结构,具体的说采用的是一种比较高效的特殊的平衡检索二叉树—— 红黑树结构。
C++基本语法笔记
基本语法
c++在创建变量时,必须给变量一个初始值,否则会报错。
switch语句中的表达式类型只能是整型或者字符型。
case中如果没有break,那么程序会一直向下执行。
二维数组的定义方式:
//第一种
int arr[2][3];
arr[0][0] = 1;
//第二种
int arr2[2][3] =
{
{1,2,3},
{4,5,6}
};
//第三种
int arr3[2][3] = { 1,2,3,4,5,6 };
指针
我们可以通过&符号获取变量的地址。
我们可以利用指针记录地址。
对指针变量解引用,可以操作指针指针指向的内存。
空指针:指针变量指向内存空间中编号为0的空间。
空指针是一个特殊的指针值。
空指针是指可以确保没有指向任何一个对象的指针。通常使用宏定义NULL来表示空指针常量值。NULL就代表系统的0地址单元。
空指针确保它和任何非空指针进行比较都不会相等,因此进程作为函数发生异常时的返回值使用。
空指针用途:用于初始化指针变量。(空指针指向的内存是不可访问的,不可解引用)。
野指针:指针变量指向非法的内存空间。不可访问。
const修饰指针有三种情况。
-
const修饰指针 ——常量指针
//const修饰的是指针,指针指向可以改,指针指向的值不可以更改 const int * p1 = &a; p1 = &b; //正确 //const修饰的是常量,指针指向不可以改,指针指向的值可以更改 int * const p2 = &a; //p2 = &b; //错误 *p2 = 100; //正确 //const既修饰指针又修饰常量 const int * const p3 = &a; //p3 = &b; //错误 //*p3 = 100; //错误
-
const修饰常量——指针常量
-
const既修饰指针,又修饰常量
指针和数组
当指针指向一维数组时,指向的是数组的第一个元素,通过解引用即可获得第一个元素的值。
当数组名传入到函数作为参数时,被退化为指向首元素的指针。
结构体指针
通过指针访问结构体中的成员。
结构体中的const使用场景:加入const防止误操作,即不小心修改结构体的值。数组前加const也是同样的道理。
核心编程
内存分区编码
c++在运行前分为全局区和代码区
代码区的特点是共享和只读
全局区存放全局变量\静态变量\常量
常量区存放const修饰的全局常量和字符串常量
局部变量存放在栈区
利用new创建的数据,会返回该数据对应类型的指针
引用
给变量起个别名。
引用必须初始化,引用在初始化后,不可改变。
可以让引用做函数参数,可简化指针修改实参。
总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单
引用可以作为函数返回值。
不要返回局部变量引用。
引用的本质就是一个指针常量。
常量引用(const int& a),用于防止误操作。
函数提高
如果某个位置参数有默认值,那么从这个位置往后,必须都要有默认值。
如果函数申明有默认值,那么函数实现的时候就不能有默认参数。
c++的函数的形参列表中可以有占位参数,用来占位,调用函数时必须填补该位置。
函数重载条件:
-
同一个作用域下
-
函数名称相同
-
函数参数类型不同或者个数不同或者顺序不同
函数的返回值不可作为函数重载的条件
引用可以作为重载条件
类和对象
c++面向对象三大特性:封装、继承、多态
三种访问权限:
- 公共权限 public 类内可以访问 类外可以访问
- 保护权限 protected 类内可以访问 类外不可以访问
- 私有权限 private 类内可以访问 类外不可以访问
struct和class的区别:其唯一区别在于默认的访问权限不一样。(struct默认权限为公共,class的默认权限为私有)
一个对象或者变量没有初始状态,对其使用后果是未知的,同理,在使用完之后,没有及时清理,也会造成安全问题。
c++中利用了构造函数和析构函数来解决上述问题,,这两个函数都会被编译器自动调用。完成对象初始化和清理工作。
-
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
构造函数语法:
类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
-
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
析构函数语法:
~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
构造函数分类:
-
按照参数分类为有参构造和无参构造,无参又称为默认构造函数
-
按照类型分类为普通构造和拷贝构造
默认情况下,c++编译器至少给一个类添加三个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
-
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
-
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
深拷贝和浅拷贝:
- 浅拷贝:简单的赋值拷贝操作
- 深拷贝:在堆区重新申请空间,进行拷贝操作
初始化列表:用来初始化属性
- 语法:
构造函数():属性1(值1),属性2(值2)... {}
当类中成员是其他类对象时,我们称该成员为对象成员。构造的顺序是先调用对象成员的构造,再调用本类的构造。析构顺序与构造相反。
静态成员变量的两种访问方式:通过对象访问,通过类名访问。私有权限访问不到。
在c++中成员变量和成员函数是分开存储的。只有非静态成员变量才属于类的对象上。非静态成员变量占用对象空间;静态成员变量和函数不占对象空间。
c++提供this指针,指向被调用的成员函数所属的对象。
c++中的空指针也是可以调用成员函数的,但是也要注意有没有用到this指针。如果用到this指针就不可以。
成员函数后加const后我们称这个函数为常函数。常函数内不可更改成员属性。成员属性声明是加关键字mutable后,在常函数中依然可以修改。
声明对象前加const称该对象的常对象。常对象只能调用常函数。常对象不能修改成员变量的值,但是可以访问。常对象可以修改mutable修饰成员变量。
友元
目的:让一个函数或者类访问另一个类中的私有成员。
关键字:friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
运算符重载
对已有的运算符重新进行定义,赋予另一种功能,以适应不同的数据类型。
继承
可以减少重复的代码
class A : public B
A类称为子类或者派生类
B类称为父类或者基类
继承一共三种方式
-
公共继承
-
保护继承
-
私有继承
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qgyVoKDH-1634199259949)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20210626222035362.png)]
父类中私有成员也是被子类继承下去了,只是有编译器给隐藏后访问不到。
子类继承父类后,当创建子类对象,也会调用父类的构造函数。(先调用父类构造函数,再调用子类构造函数,析构顺序相反)。
当子类与父类出现同名的成员时,如何通过子类对象,访问到子类或父类中同名的数据呢?
答:访问子类同名成员,直接访问即可;访问父类同名成员,需要加作用域。
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
多继承
语法:class 子类 :继承方式 父类1 , 继承方式 父类2...
多继承可能会引发父类中同名成员出现,需要加作用域区分。(c++实际开发中不建议用多继承)
菱形继承问题:
-
羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
-
草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
总结:
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义。
- 利用虚继承可以解决菱形继承问题。
多态
多态分为两类:
- 静态多态:函数重载 和 运算符重载属于静态多态, 复用函数名。
- 动态多态:派生类和虚函数实现运行时多态。
静态多态和动态多态区别:
- 静态多态的函数地址早绑定——编译阶段确定函数地址
- 动态多态的函数地址晚绑定——运行阶段确定函数地址
多态满足关系:
- 有继承关系
- 子类重写父类中的虚函数(函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。只有加了virtual关键字,子类重写才有意义)
多态使用:
- 父类指针或着引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称之为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
纯析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方式:将父类中的析构函数改为纯析构或者纯虚析构。
虚析构和纯虚析构的共性:
- 可以解决父类指针释放子类对象。
- 都需要有具体的函数实现
虚析构和 纯虚析构的区别:
- 如果是纯虚析构,则该类属于抽象类,无法实例化对象。
总结:
1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
3. 拥有纯虚析构函数的类也属于抽象类
C++提高编程
模板
特点:
- 模板不可以直接使用,只是一个框架
- 模板的通用并不是万能的
两类模板机制:
-
函数模板
建立一个通用函数,其函数返回值类型和形参类型可以不具体制定,用一个虚拟的类型来代表。
template<typename T> 函数声明或定义
总结:
- 函数模板利用关键字template
- 使用函数模板有两种方式:自动类型推导、显示指定类型
- 模板的目的是为了提高复用性,将类型参数化。
注意事项
- 自动类型推导,必须推导出一致的数据类型T
- 模板必须确定出T的数据类型,才可以使用
普通函数与函数模板的区别:
- 普通函数调用时可以发生自动类型转换(隐式类型转换)
- 函数模板调用时,如果利用自动类型推导,则不会发生隐式类型转换
- 如果利用显示指定类型的方式,可以发生隐式类中转换
利用具体化的模板,可以解决自定义类型的通用化
-
类模板
类模板作用:建立一个通用类,类中的成员数据类型可以不具体制定,用一个虚拟类型来代表。
-
类模板和函数模板的区别:
类模板中没有自动类型推导的使用方式
类模板在模板参数列表中可以有默认参数
总结:
- 类模板使用只能用显示指定类型方式
- 类模板中的模板参数列表中可以有默认参数
类模板中成员函数创建时机:
- 普通类中的成员函数一开始就可以创建
- 类模板中的成员函数只有在使用时才创建
类模板函数对象做函数参数:
-
-
类模板与继承:
当类模板碰到继承时,需要注意一下几点:
- 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
- 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定出父类中T的类型,子类也需变为类模板
STL初识
STL六大组件:
容器、算法、迭代器、仿函数、适配器、空间配置器
容器:
-
序列式容器
强调值的排序,序列容器中的每个元素均有固定的位置。
-
关联式容器
二叉树结构,各元素之间没有严格的物理上的顺序关系。
vector
vector<int>::iterator pBegin = v.begin();//指向vector中的第一个元素
vector<int>::iterator pEnd = v.end();//指向vector中的最后一个元素的下一位置。
while (pBegin!=pEnd) {
cout << *pBegin <<endl;
pBegin++;
}