一、 语法:
1.指针常量/常量指针
- 指针常量:顾名思义,指针是一个常量, 所以指针的指向不能改变,指针指向的值可以改变。
- 常量指针:顾名思义,指向常量的指针,指针的指向可以改变,指针指向的值不能改变。
2. 内存模型
- 代码区:
- 用户写的所有代码都会放在代码区,二进制格式。
- 代码区是共享的:相对于线程来说,代码区内的所有代码都是共享的,因为有可能用户会多次执行exe文件,开辟多次线程,所有多个线程可以共享同一份代码数据即可运行程序。
- 代码区是只读的:使其只读的原因是防止程序意外地修改了它的指令。
- 全局区:
存放全局变量和静态变量以及常量,该区域的数据在程序结束后由操作系统释放.-
全局变量:只要不是写在函数体中的变量都称之为全局变量。
-
静态变量:
在普通变量前加上static,属于静态变量。局部静态变量和全局静态变量都属于全局区:
-
常量:
-
字符串常量:
只要是用” xxx” 的都是字符串常量 -
const修饰的量:
const修饰的全局变量:属于全局区
cosnt修饰的局部变量:在方法体中属于局部变量不属于全局区
-
-
总结:C++在程序运行前需要在内存中开辟代码区、全局区。全局区存放全局变量,static修饰的静态变量,常量(字符串常量,全局常量)
-
栈区:
由编译器自动分配释放, 存放函数的参数值,局部变量等。
注意事项:不要返回局部变量的地址,因为方法体中的局部变量是分配在栈区中的,只要方法执行完后,栈区开辟的数据空间就由编译器自动释放
-
堆区:
由程序员分配和释放,若程序员不释放,程序全部结束时由操作系统回收
利用new在堆区开辟内存:
利用delete释放内存
总结:不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程
3. 引用
-
引用的本质:引用的本质就是给一个变量起别名
引用必须在定义的时候就初始化。引用在初始化后,不可以改变
-
引用的底层实现:引用的本质就是定义一个指针常量来指向a,所以这也能解释引用不能修改的原因。
-
引用在C++中的使用:
-
引用在函数中做值传递的应用:
函数中参数的值传递一般有三种方式:
值传递:
地址传递:
引用传递:
常量引用作为新参:表示传来的参数不能修改,只能读取:
引用传递和地址传递都能实现数值交换的需求,主要原理在于他们传递的都是地址。通过地址操作原来的栈中的a、b数据。而通过值传递则不能实现,因为值传递还是会在栈中分配另一份空间,函数中也是操作另一份空间中的内容,原空间中的值不变。
- 引用在函数中做返回值的应用:
注意:不能将局部变量的引用返回
引用可以作为函数的左值:
-
4. C++中面向对象的三大特性
-
封装:
-
封装的意义以及访问权限:
将属性和行为作为一个整体,表现生活中的事物。可以给属性和行为加以权限控制
三种权限
①公共权限 public 类外可以访问 子类继承能访问
②保护权限 protected 类外不可以访问 子类继承可以访问
③私有权限 private 类外不可以访问 子类继承不可以访问 -
类的构造与析构:
//拷贝构造
Person p3 = Person(p2);
Person p5 = p4; -
深度拷贝和浅拷贝:
简单来说浅拷贝就是在对象进行拷贝的时候对对象中的属性进行简单的赋值操作,例如对象L1属于L类,L1中有一个属性m是指向在堆区开辟的一块内存空间。当调用L L2(L1)进行拷贝操作时,由于是浅拷贝,所以直接将L1中的值赋给L2,故L2中的m属性会直接将L1中的m属性的值拷贝过来,那么最终L1、L2的m属性都指向堆区的同一份空间。如果哪天L1进行析构函数的调用,将自身的所有属性都释放掉,包括L1.m指向的那一块堆区空间。那么L1释放掉后,若L2再去执行析构时也会和L1一样去释放自身的所有属性,由于L1已经把堆区那一片空间释放掉了,所以L2再去释放就会报错。根本原因在于L2是进行浅拷贝的L1值,导致L2、L1指向同一份空间。正确的做法是应该采用深拷贝,L2在拷贝L1时,同样给L2.m属性分配一块堆区空间。
编译器默认提供的拷贝方法就是浅拷贝。 -
类中的静态成员
所有对象共享同一份数据,在编译阶段分配内存,类内声明,类外初始化
(1)类的静态成员是属于类而不属于对象,所以他不是类的单个对象所有。
(2)静态成员只存在一个,不像普通的成员,每创建一个对象,就会创建一组普通的成员。
(3)父类中定义了静态成员,则整个继承体系中只有一个这样的成员,无论派生出多少个子类。静态成员是存储在全局区的。
-
this
- C++中,类内的成员变量和成员函数分开存储。只有非静态成员变量才属于类的对象上。因为C++中成员变量和成员函数是分开存储的,类中每一个非静态函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码,那么问题是:这一块代码是如何区分那个对象调用自己的呢?c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象。this指针是隐含每一个非静态成员函数内的一种指针,this指针不需要定义,直接使用即可。
- this指针的用途:
当形参和成员变量同名时,可用this指针来区分。
在类的非静态成员函数中返回对象本身,可使用return *this。 - C++中允许空指针调用成员函数,前提是该方法中没有用到this。依据上面所讲,C++中所有对象的方法都是统一放在一个地方,当某个对象调用方法时会隐含的将this指针传入,如果方法中用到了该对象的属性,那么会默认的用this去访问,若指针为空那么this就访问不到。
因为showPersonAge中访问了非静态成员变量m_Age,而p又没有初始化,所以this = NULL。最终访问错误。
-
常函数与常对象
- 常函数:
成员函数后加const后我们称为这个函数为常函数
常函数内不可以修改该类的对象中成员的属性
成员属性声明时加关键字mutable后,在常函数中依然可以修改 - 常对象:
对象后面加const后称之为常对象,常对象不能修改对象中的属性值,并且常对象只能调用常函数。
- 常函数:
-
友元
生活中你的家有客厅(Public),有你的卧室(Private)客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去但是呢,你也可以允许你的好闺蜜好基友进去。在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术友元的目的就是让一个函数或者类访问另一个类中私有成员友元的关键字为 friend友元的三种实现。 -
运算符重载
-
-
继承:
继承的语法:class 子类 : 继承方式 父类
继承方式一共有三种,子类B继承父类A以不同的方式继承会得到不同的效果
公共继承:A中属性的访问权限到了B中还是不变。
保护继承:A中private变量访问权限到了B中不变,其他的都变为protect
私有继承:A中所有的属性都变为private注:子类会把父类无论什么访问权限的属性都继承下去。
如果有子类中有同名的成员属性,那么想要访问父类中的属性就必须添加作用域来调用:
构造、析构顺序:继承中是先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。 -
多态:
-
多态问题涉及到地址早绑定、晚绑定问题。
地址早绑定:在编译阶段就确定了函数地址。
地址晚绑定:如果想要地址晚绑定则需要指定virtual关键字
-
多态满足条件:
①有继承关系
②子类重写父类中的虚函数
③父类指针或引用指向子类对象 -
动态多态底层实现原理:
我们已经知道:类的成员函数不属于某个对象,该函数是放在代码区。
当Animal类中的speak()方法没有加virtual关键字时,调用sizeof(Animal)结果是1,因为C++编译器会给每个空对象也分配一个字节空间,为了区分空对象占内存的位置,每个空对象也应该有一个独一无二的内存地址
而如果加上virtual关键字,那么c++会为该类维护一个虚函数表(vftable),并用一个虚函数表执政(vfptr)指向该表
所以只要类中有虚函数,那么c++就会开辟一个空间vftable存放这些虚函数的地址如&WLM::fun1(),&WLM::fun2(),并且生成一个vftptr指向该表,将vftptr隐含的保存到该类中,所以sizeof(WLM)=8 多出来一个vftptr的空间
如果发生继承,那么子类中也会将该vfptr也复制一份到子类对象中
———>
当子类重写父类中的虚函数时,子类中的虚函数表内部会替换成子类的重写后的虚函数地址。
总结:因为我们在父类中写了virtual虚函数,父类的类内部结构就会发生变换,生成了一个vfptr虚函数指针,指向虚函数表,表中记录了类中的虚函数入口地址,当子类继承父类后,子类内部也会有一个虚函数指针,同样也指向属于子类的虚函数表。若子类重写了父类中的虚函数,那么子类中的虚函数表中的虚函数入口地址则被子类替换成重写后的虚函数入口地址。如果发生多态,则调用的则是虚函数表中子类重写的虚函数。
-
5. 纯虚函数和抽象类
- 纯虚函数:
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0
当类中有了纯虚函数,这个类也称为抽象类 - 虚析构的作用:
当我们实现多态时,如果子类中的属性有指向某块堆区,delete时是不会走子类中的析构,而是会去走父类中的虚构函数
如上:Animal animal 实现多态,但delete animal不会走Cat中的虚构
解决方式:将父类中的析构函数改为虚析构
—>
这样 delete animal 后,就会走子类Cat的虚析构函数。
6. 文件操作
-
定义:
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,所以需要通过文件可以将数据持久化。
C++ 中对文件的操作需要包含头文件 < fstream >
文件类型分为两种: -
文件类型:
文本文件 - 文件以文本的ASCII码形式存储在计算机中
二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂
操作文件的三大类:
< ofstream>:写操作
< ifstream>: 读操作
< fstream>: 读写操作 -
文件操作步骤:
①包含头文件:#include < fstream >;
②创建流对象:ofstream ofs; 或 ifstream ifs;
③打开文件: ofs.open(“文件路径”,打开方式);
④读/写 数据:ofs << “写入的数据”;
⑤关闭文件: ofs.close();
7. 模板与泛型
-
函数模板:
普通函数与函数模板区别:
①普通函数调用时可以发生自动类型转换(隐式类型转换)
②函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换。如果利用显示指定类型的方式,可以发生隐式类型转换
③建议使用显示指定类型的方式调用函数模板,因为可以自己确定通用类型T
④如果函数模板和普通函数都可以实现,优先调用普通函数,可以通过空模板参数列表来强制调用函数模板
⑤函数模板也可以发生重载,如果函数模板可以产生更好的匹配,优先调用函数模板
-
类模板:用template+typename 或 template+class都行:
类模板中成员函数创建时机:
普通类中的成员函数一开始就可以创建,类模板中的成员函数只在调用时才创建
-
类模板作为函数参数的调用更加优雅:
-
类模板与继承:
当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
如果不指定,编译器无法给子类分配内存
如果想灵活指定出父类中T的类型,子类也需变为类模板
-
类模板分文件编写:
类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到
将声明和实现写到同一个文件中,并更改后缀名为.hpp,hpp是约定的名称,并不是强制
8. STL
SLT六大组件:容器、算法 、迭代器、仿函数、适配器、空间配置器
-
容器:vector、list、deque、set、map等
-
vector:
向容器中插入数据:v.push_back()
遍历的三种方法:
vector与数组的区别:不同之处在于数组是静态空间,而vector可以动态扩展。并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间。 -
list
-
deque
-
set
-
map
-
-
算法
-
迭代器
-
仿函数