此为学习笔记,参考的教程链接:https://www.bilibili.com/video/BV1et411b73Z?p=84
代码笔记链接:https://github.com/JamieLL/cpp_beginner
1. 内存分区
- 代码区
- 共享、只读
- 全局区:全局变量、静态变量(static)、常量(字符串、const修饰的全局常量)
- 栈区:编译器自动分配释放,如函数参数值、局部变量
- 不要返回局部变量的地址,栈区数据在函数执行后自动释放
- 堆区:程序员分配释放,程序结束时系统也会回收
- 用new在堆区开辟内存:
int* p = new int(10);
- 释放:
delete p;
,delete[] arr;
- 用new在堆区开辟内存:
2. 引用
给变量起别名:数据类型 &别名 = 原名,int &b = a;
- 引用必须初始化,且之后不能更改
- 引用传递,可以用形参修饰实参,即可以不用指针的地址传递
- func(int& a, int& b){}, 调用时func(a, b)
- 引用作为函数返回值
- 不要返回局部变量的引用
- 函数的调用可以作为左值
int& aka() {
static int a = 10;
return a;}
int main() {
int &ref = aka();
aka() = 1000; //赋值操作,于是ref2 = 1000;
}
- 引用的本质是一个指针常量
int& ref1 = a
等价于int* const ref1 = &a;
ref1 = 20;
,内部发现ref1是引用后,等价于*ref1 = 20;
- 常量引用:const用来修饰形参,防止误操作,
func(const int& a)
3. 函数提高
- 形参可以有默认值
- 如果某个位置有默认值了,其后的所有形参都必须有默认值
- 函数声明和实现只能有一个有默认参数
- 占位参数:函数中只写数据类型,在调用时必须补上,
int fun(int a,int)
,int func(int a,int =10)
- 函数重载:同一个作用域下,函数名相同, 函数参数类型不同或者个数不同或者顺序不同
- 引用作为重载条件时:int& a和const int& a;当传入的是变量时调前者,常量调后者。
- 函数重载碰到默认参数:产生歧义,报错
4. 类和对象
面向对象:封装、继承、多态
4.1 封装
定义
- 类中的属性和行为统称为成员
- 属性:成员属性、成员变量
- 行为:成员函数、成员方法
- 实例化对象
- 访问权限:
- public公共:成员类内可以访问,类外可以访问
- protected保护:成员类内可以访问,类外不可以访问,子类可以访问
- private私有:成员类内可以访问,类外不可以访问,子类不可以访问
注意事项
- struct和class的区别:struct默认权限是public,class默认权限是private
- 成员属性设置为private:可以自己控制读写权限、对于写权限可以检测数据的有效性
4.2 对象特性
构造函数和析构函数
- 构造函数:创建对象时为对象的成员属性赋值,由编译器自动调用,无须手动
- 类名() {},可以有参数,可重载
- 无参构造、有参构造、拷贝构造
- 调用默认(无参)构造函数时,不要加(),否则会认为是函数声明
- 值传递时会调用拷贝构造,值方式返回局部对象时也会调用拷贝构造
- 默认情况,c++至少给一个类添加默认构造函数(无参)、默认析构函数(无参)、默认拷贝构造函数、operator=
- 若用户定义有参构造,则c++不再提供默认无参构造,但会提供默认拷贝构造
- 若用户定义拷贝构造,则c++不再提供其他构造函数
- 析构函数:对象销毁前系统自动调用,执行一些清理工作
- ~类名() {},不可以有参数,不能重载
其他
- 深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝
- 可能产生的问题:堆区的内存重复释放
- 深拷贝:在堆区重新申请空间,进行拷贝
- So,如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题
- 初始化列表
初始化列表初始化属性:Student(int a, int b) :m_a(a), m_b(b) {}
- 类对象作为类成员
- 当其他类对象作为本类成员,构造时先构造其他类,再自身;析构与构造相反
- 静态成员
- 静态成员变量:类内声明,类外要初始化
- 静态成员函数:所有对象共享同一个函数,静态成员函数只能访问静态成员变量
- 可以直接用类名访问
Person:func();
- 可以直接用类名访问
- 类内的成员变量和成员函数是分开存储的,只有非静态成员变量才属于类的对象上
- c++会给空对象也分配一个字节空间,是为了区分空对象占内存的位置;有成员时就算成员的大小
- this指针:指向被调用的成员函数所属的对象
- 当形参和成员变量同名时,用this区分
- 在类的非静态成员函数中返回对象本身,可
return *this;
- 空指针访问成员函数
- 但是注意此时空指针不能访问变量(报错
- const修饰成员函数:常函数
void func() const {}
,本质修饰的是this指针- 不能修改成员属性,但若成员属性声明时加了mutable就可以修改
- const修饰的对象为常对象,只能调用常函数
4.3 友元
friend
,让一个函数或者类访问另一个类的私有成员
- 三种实现:全局函数、类、成员函数
- 代码顺序:a类是b类的friend,声明b->定义a->定义b->实现b->实现a,类中的友元函数得在类外实现,否则报错
friend function本质上来说是global的,不是class scope的,所以只能在类里面声明,在类外面定义
4.4 运算符重载
对已有的运算符重新定义,以适应不同的数据类型
- 可通过成员函数、全局函数重载
- 运算符重载也可以发生函数重载
- 对内置的数据类型的运算不能改变
- 加号+
- 左移运算符<<
可输出自定义的数据类型,配合友元
- 一般不用成员函数来重载左移运算符:无法实现cout在左侧
- ostream cout
- 递增运算符++
可实现自己的整型数据
- 区分前后置:前operator++(),后operator++(int)
- 前置返回引用,后置返回值
- 赋值运算符=
- 如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
- 关系运算符>, <, >=, <=, ==
可以让两个自定义类型对象进行对比操作 - 函数调用运算符()
- 由于重载后的使用方法很像函数的调用,因此称为仿函数
- 仿函数没有固定写法,参数灵活
4.5 继承
- 上一级的共性+自己的特性
- 好处:减少重复代码
- 语法:
class Son : public Father {};
- 子类也称为派生类,父类也称为基类
- 继承方式:
- 公共继承:父中的public和protected原样继承
- 保护继承:父中的public和protected继承为protected
- 私有继承:父中的public和protected继承为private
- 父类中所有的非静态成员属性都会被子类继承
- 关于父类的private,会被继承(体现在sizeof),只是被隐藏了,访问不到
- 用“Developer Command Prompt for VS 2019”验证:
- 进入cpp所在路径
dir
查看目录cl /d1 reportSingleClassLayout类名 "文件名"
可查看该class的结构、size等信息
- 继承中的构造和析构顺序:
- 父构造、子构造、子析构、父析构(析构与构造顺序相反)
- 继承同名成员:
- 子类成员直接访问,父类同名成员加作用域
s.Base::m_A
- 如果子类中出现了与父类同名的成员函数,则会隐藏父类中所有的同名成员函数,故访问父类中的都得加作用域(就算参数不同也不行)
- 子类成员直接访问,父类同名成员加作用域
- 继承同名静态成员:
- 子类成员直接访问,父类同名成员加作用域
- 通过子类类名访问父类作用域下的静态成员:
Son::Base::m_A
- 多继承
- 语法:
class Son : public Base1, public Base2{};
- 可能会引发父类中同名成员出现,要加作用域区分,故c++实际开发中不建议多继承
- 语法:
- 菱形继承/钻石继承
- 两个父亲继承同一个爷爷,又有一个儿子同时继承两个父亲
- 引发的问题:儿子会有两份爷爷的数据,要加作用域;但是儿子只需要一份
- 利用虚继承,解决菱形继承的问题
class Son:virtual public Base{};
- 此时儿子只有一份爷爷的数据了,但size是两份
- 继承的是vbptr虚基类指针,指向vbtable
4.6 多态
- 分为两类:
- 静态多态:函数重载、运算符重载
- 动态多态:派生类和虚函数实现运行时的多态
- 区别:静态多态的函数地址早绑定(编译阶段),动态多态的函数地址晚绑定(运行阶段)
- 动态多态满足条件:有继承关系,子类重写父类的虚函数
- 重写指:函数返回值类型、函数名、参数列表完全相同
- 动态多态使用:父类的指针或者引用执行子类对象
- 多态原理:
- vfptr虚函数(表)指针,指向vftable虚函数表
- 指针占4个字节
- vftable表内部记录虚函数的地址
- 当子类重写父类的虚函数,子类中的虚函数表内部会替换成子类的虚函数地址
- 当父类的指针或者引用指向子类对象时,发生多态
- vfptr虚函数(表)指针,指向vftable虚函数表
- 多态优点:代码组织结构清晰、可读性强、利于前期和后期的扩展以及维护
- 纯虚函数和抽象类
- 纯虚函数:
virtual 返回值类型 func(...) = 0;
- 当类中有了纯虚函数,此类称为抽象类
- 抽象类特点:无法实例化对象,子类必须重写抽象类中的纯虚函数,否则也属于抽象类
- 纯虚函数:
- 虚析构和纯虚析构
- 若子类有申请堆区,则父类指针在释放时无法调用到子类的析构代码,内存泄漏,故将父类的析构函数改为虚析构或纯虚析构
- 共性:可以解决父类指针释放子类对象,都需要具体的函数实现
- 区别:若是纯虚析构,则该类属于抽象类,无法实例化对象
- 虚析构:
virtual ~类名(){}
- 纯虚析构
vitual ~类名() = 0;
,类名::~类名(){}
- 记得要具体实现,且在类外实现
5 文件
#include <fstream>
- ofstream:写
- ifstream:读
- fstream:读写
- 文本文件:以文本的ASCII码形式存储
- 二进制文件:以文本的二进制形式存储
5.1 文本文件
- 写文件
- 头文件:
#include <fstream>
- 创建流对象:
ofstream ofs;
- 打开文件:
ofs.open("路径",打开方式);
ios::in
读文件ios::out
写文件ios::app
追加方式写文件ios::trunc
若文件存在先删除,再创建ios::binary
二进制方式ios::ate
初始位置在文件尾- 可配合使用
|
,如ios::binary | ios::out
- 写数据:
ofs << "写入的数据";
- 关闭文件:
ofs.close();
- 头文件:
- 读文件步骤类似,有4种读取方式(见代码)
5.2 二进制文件
ios::binary
- 写文件
- 函数原型
ostream& write(const char* buffer, int len);
- 字符指针buffer指向内存中一段存储空间,len是读写的字节数
- 函数原型
- 读文件
- 函数原型
istream& read(char *buffer, int len);
- 函数原型