有人查漏补缺,有人精卫填海,有人开天辟地(bushi
内存的分区模型
代码区:存放函数体的二进制代码
全局区:存放全局变量、静态变量、常量
栈区:由编译器自动分配/释放,存放函数参数和局部变量等
堆区:由程序员分配和释放,若未释放则在程序结束时由操作系统回收
代码区的特点:共享(多次执行只需要一份代码即可),只读(防止意外修改)
全局区的特点:数据在程序结束后由操作系统释放
栈区的特点:保存在栈区的数据在函数执行完即自动释放
堆区的特点:用new开辟,由程序员指定分配
1 | int *p=new int(10);//在堆区开辟了一块int的内存,里面存的值为10 |
引用(给变量起别名),指向同一片内存空间
意义:
要求:
- 一定要初始化
- 一旦初始化了,就不可以更改了
1
2int a;
int b=&a;
参数传递
- 值传递
1
2
3
4
5
6
7
8
9
10void swap(int a,int b)
{
int temp=a;
a=b;
b=temp;
}
int a=10;
int b=10;
swap(a,b);
//实参的值不会被改变 - 地址传递
1
2
3
4
5
6
7
8
9
10void swap(int* a,int* b)
{
int temp=a;
a=b;
b=temp;
}
int a=10;
int b=10;
swap(&a,&b);
//实参的值会被改变 - 引用传递
1
2
3
4
5
6
7
8
9
10
11void swap(int& a,int& b)
{
//使用引用接收参数
int temp=a;
a=b;
b=temp;
}
int a=10;
int b=10;
swap(a,b);
//实参的值会被改变
引用作函数返回值
含义:如果一个函数的返回值是引用,那么这个函数可以作为左值
- 不要返回局部变量的引用引用的本质:一个指针常量,指针不可以修改,但是指针指向的值可以修改
1
2
3
4
5
6
7
8
9int& yy()
{
static int a=10;
return a;
}
int& ref=yy()
yy()=1000;
cout<<ref;
//此时输出的是1000
由编译器完成(引用→指针常量)的过程
常量引用:
使用场景:用来修饰形参,使其只读,防止误操作1
void yy(const int &val);
函数进阶
函数的默认参数
类型 名(参数名=默认值){}
1 | void qwq(int a,int b=20){return a+b;} |
- 若某位置有默认值,则该位置往后必须全部有默认参数(规定)
- 声明和实现只可以有一个有默认参数(规定)
- 如果函数参数值>传入参数值,则从第一个参数开始向后填充,若某参数未被填充且没有默认值,则会报错
函数的占位参数
调用函数时必须填补该位置
1 | void qwq(int a,int){return a+b;} |
函数重载
满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数(类型/个数/顺序)不同
- 函数返回值并不作为函数重载的条件
注意事项: - 引用可以作为重载的条件
- 重载遇到默认参数时,警惕UB(二义性)报错
面向对象
基本特性:继承、多态、封装
- struct和class的区别:
- struct的默认权限为公共
- class默认权限为私有
构造函数的分类和调用
调用方式:
1 | class qwq{ |
拷贝构造函数的调用时机
- 使用一个对象来初始化另一个对象
- 以值传递的方式给函数参数赋值
自动调用拷贝构造函数创造副本 - 以值方式返回局部对象
此时被返回的对象会被拷贝构造
构造函数调用规则
- 默认存在构造函数、析构函数、拷贝构造函数
- 如果定义有参构造函数,只会提供默认拷贝构造
- 如果定义拷贝构造函数,不会提供其他构造函数
深拷贝和浅拷贝
浅拷贝:简单的复制拷贝,默认的拷贝构造函数拷贝方法
容易带来的问题:若存在堆区空间的操作,容易造成重复释放与重复操作
深拷贝:在堆区重新申请空间,进行数据拷贝
初始化列表
qwq():a(10),b(20){}
//一个让a的默认值为10,b的默认值为20的无参构造函数
qwq(int a,int b):a(a),b(b){}
//一个让a的值为a,b的值为b的有参构造函数
类对象作为类成员
1 | class A{} |
静态成员
静态成员变量特点:
- 所有对象共享统一数据
- 在编译阶段即分配内存
- 类内声明,类外初始化
- 仍然存在访问权限静态成员变量访问方式:
1
2
3
4//类内:
static int a;
//类外:
int qwq::a=100; - 通过对象进行访问
1
2qwq q;
cout<<q.a; - 通过类名进行访问
1
cout<<qwq::q;
静态成员函数特点:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
- 存在访问权限
静态成员函数调用方法: - 通过对象进行访问
1
2qwq q;
cout<<q.a(); - 通过类名进行访问
1
cout<<qwq::q();
C++对象内存模型和this指针
只有非静态成员变量才属于类的对象
成员变量和成员函数是分开存储的
静态成员变量和成员函数只有一份拷贝
this指针:指向被调用的成员函数所属的对象
奇怪的用法:可以使用空指针调用成员函数
1 | qwq* p=NULL; |
const修饰成员函数
- const修饰的函数成为常函数
- 常函数不可以修改成员属性
- 可以修改有关键字mutable的成员变量
友元
关键字: friend
作用:让一个函数或者类区访问另一个类的私有成员
三种实现方式:
- 全局函数作友元
1
2
3
4
5
6
7
8
9
10
11
12
13class qwq
{
friend void visit(qwq *q);
//此时,visit函数可以访问qwq的私有成员
public:
...
private:
...
}
void visit(qwq *q)
{
...
} - 类作友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class qaq;
//提前声明,之后实现
class qwq
{
friend class qaq;
//qaq类作为qwq类的友元,可以访问私有成员
public:
...
private:
int a;
}
class qaq
{
public:
void visit(qwq *q)
{
cout<<q->a;
}
private:
...
} - 成员函数作友元
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class qaq;
//提前声明,之后实现
class qwq
{
friend void qaq::visit();
//qaq类下的visit作为qwq类的友元,可以访问私有成员
public:
...
private:
int a;
}
class qaq
{
public:
void visit(qwq *q)
{
cout<<q->a;
}
private:
...
}
运算符重载
作用:对已有的运算符进行重新定义,以适应不同的数据类型
函数名:operator*(被重载的运算符)
注意:
- 对于内置的数据类型的运算是不可以重载的
- 不要滥用,尽量让运算符和实际作用相符
- 加号运算符重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Person
{
//通过成员函数重载
Person operator+(Person *p)
{
//左值为this指针指向的类
//右值为p指针指向的类
Person temp;
temp.p_a=this->p_a+p->p_a;
temp.p_b=this->p_b+p->p_b;
return temp;
}
private:
int p_a;
int p_b;
}
//通过全局函数重载
Person operator+(Person &p1,Person &p2)
{
Person temp;
temp.p_a=this->p_a+p->p_a;
temp.p_b=this->p_b+p->p_b;
return temp;
}
Person p3 = p1 + p2; - 左移运算符重载
1
2
3
4
5
6
7
8
9
10
11ostream operator(ostream &cout,Person &p)
{
//有一点像是java中.toString()的意思
cout<<p.a<<" "<<p.b;
return cout;
//返回类型为ostream
//如果不是ostream,不可以在cout中继续往后追加
//那么如果输出的时候要访问私有函数呢?
//友元!friend!
//在类内写friend ostream operator(ostream &cout,Person &p),使该方法成为原有类的友元
} - 递增运算符重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//重载前置++运算符
//写在类内
Person& operator++()
{
p_a++;
return *this;
}
//重载后置++运算符
Person& operator++(int)
//int是占位参数
//c++无法通过返回类型来实现多态,因此需要以参数类型来区分函数
{
Person temp=*this;
//先记录,在自增,再返回记录值
p_a++;
return temp;
}
//前置递增返回引用,后置递增返回值
//原因:如果后置递增返回对象,那么temp会在函数执行后被销毁,引用会成为野指针 - 赋值运算符重载
知识点:C++编译器会给一个类添加赋值运算符=对属性进行值拷贝(默认为浅拷贝) - 复习!浅拷贝在什么情况下会出问题!
- 答:涉及到堆区内存申请/释放的时候
1 | //赋值运算符注意事项 |
- 函数调用运算符()重载
//因为长得真的很像自定义函数,又称之为仿函数
//仿函数没有固定写法,非常灵活1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Person
{
//通过成员函数重载
Person operator()(String text)
{
cout<<text;
}
}
int main()
{
Person p;
p("qwq!");
//此时会打印qwq!
//注意上文中括号前为对象名,不是类名
}
继承
基本语法:
class 派生类名:继承方式1 基类1,继承方式2 基类2……
C++中一个类可以由多个类派生而来
多继承中如果涉及到同名问题,加作用域以访问不同父类中的成员
但是在实际开发中不建议使用多继承
继承方式
继承后private成员不可用,成员权限会有所改变
- 公有继承
不改变权限 - 私有继承
public&protected->private - 保护继承
public->protected
继承中的对象模型
基类所有的非静态成员变量,在派生类中仍然存在,只是被编译器隐藏了
继承中构造和析构顺序
继承后在类初始化时,基类和派生类的构造函数和析构函数都会被分别调用
具体调用顺序:先调用基类构造函数,再调用派生类构造函数,析构函数顺序相反
继承中同名成员的处理方式
在继承后,同名成员会共存(静态成员也是)
访问派生类中同名成员,可以直接访问
访问基类中同名成员,需要加作用域
如果基类和派生类中存在同名函数,父类中同名函数会被隐藏,如果要访问,需要加作用域
1 | class qaq |
菱形继承
定义:
两个派生类继承同一个基类,某个类同时继承这两个派生类
可能导致的问题:数据重复,资源浪费
解决方案:虚继承(在第一次继承的两个派生类上发生)(关键词virtual)
vbptr(Virtual Base Pointer):虚基类指针,指向虚基类表
- 指针占4个内存嗷
采用虚继承后,来自同一个上层基类的数据会只存在一份拷贝,对于直接继承的两个基类,存在一个vbptr,指向虚基类表中的偏移量,即为那一份拷贝在多少位之后
多态
- 静态多态:函数重载和运算符重载,复用函数名,依靠传入参数类型和个数区分
编译阶段确定函数地址 - 动态多态:派生类和虚函数实现运行时多态
运行阶段确认函数地址
如何实现动态多态?
函数前加virtual关键字,使其变成虚函数
class animal
{
public:
virtual void speak(){
cout<<”animal speak”;
}
}
class cat: public animal
{
public:
virtual void speak(){
cout<<”cat speak”;
}
}
class dog: public animal
{
public:
virtual void speak(){
cout<<”dog speak”;
}
}
void doSpeak(Animal &animal)//也可以是指针
{
animal.spaek();
//可以传入animal,dog,cat
//此时若调用doSpeak函数,会根据传入的参数选择调用哪一个类中的speak函数
}
内部实现
vfptr(Virtual Function Pointer):虚函数指针
指向虚函数表vftable(Virtual Function Table)
虚函数表内容:函数->入口地址
动态多态的原理:每个类都维护一个虚函数表(函数->入口地址),如果它重写了父类的函数,那么会用新函数的地址替代旧函数的地址
以上文的代码为例,cat类重写了speak函数,当cat被传入doSpeak函数中时,调用animal.speak,
此时animal是对该cat对象的引用,所以会从cat类的虚函数表中查找speak函数,查找到的就是输出”cat speak”
纯虚函数
virtual 返回值类型 函数名 (参数) =0;
有纯虚函数的类一定是抽象类,无法被实例化,子类必须重写纯虚函数
可以类比java中的abstract方法
虚析构和纯虚析构
现存的问题:使用多态时,若子类有属性在堆区需要在析构函数时调用delete释放,那么父类指针在释放时(delete 父类;)无法调用到子类的析构代码,只会调用父类的析构函数
解决方法:将父类的析构函数改为虚析构或者纯虚析构
1 | //虚析构 |
虚析构和纯虚析构的共性:
- 可以解决堆区内存释放问题
- 需要有具体的函数实现
区别: - 有纯虚析构的类为抽象类,无法被实例化,派生类必须重写析构函数
C++ 文件操作
包含库:
文件分为两种:
- 文本文件
- 二进制文件
操作文件的三大类: - ofstream 写操作
- ifstream 读操作
- fstream 读写操作
总体操作流程
创建流对象->open(“path”,way)->写入数据->close
|—-|—-|
|打开方式|解释|
|ios::in|为读文件而打开文件|
|ios::out|为写文件而打开文件|
|ios::ate|初始位置:文件尾|
|ios::app|追加方式写文件|
|ios::trunc|先删除,再创建新的|
|ios::bindry|以二进制方式打开文件|
- 可以使用|运算符,同时以多种方式打开文件
C++提高编程
泛型编程(模板)
建立一个通用模具,提高代码复用性
函数模板
1 | //template 创建模板的声明 |
使用方法:
1 | //自动类型推导 |
注意事项:
- 自动类型推导,必须推导出一致的数据类型t才可以使用
类型的一致性 - 模板必须要确定出T的数据类型才可以使用
模板对应的函数必须指出T的数据类型
函数模板和普通函数的区别:
- 普通函数调用可以发生隐式类型转换
- 函数模板如果使用自动类型推导,不会发生隐式类型转换
- 函数模板如果使用显式指定类型,可以发生隐式类型转换
函数模板调用规则
- 如果函数模板和普通函数都可以实现,优先调用普通函数
- 可以通过空模板参数列表来强制调用函数模板
- 函数模板也可以发生重载
- 如果函数模板可以更好的匹配,优先调用函数模板
总结:尽量不要让模板和普通函数功能重合,容易产生二义性
模板的局限性
对于特定数据类型(class等),需要具体化方式做特殊实现
常见相关解决方法:运算符重载(治标不治本)、具体化实现
1 | //具体化实现 |
类模板
1 | template<class NameType, ClassAgeType> |
类模板和函数模板的区别
- 类模板不可以采用自动类型推导的使用方式
1
2
3
4
5
6//以上文代码为例
int main()
{
Person("张三",20);// 不可以
Person<string, int>("张三",20);//可以
} - 类模板在模板参数列表可以有默认参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16template<class NameType, Class AgeType=int>//设置默认参数类型
class person
{
public:
Person(NameType name,AgeType age)
{
this->m_name=name;
this->m_age=age
}
NameType m_name;
AgeType m_age;
};
int main()
{
Person<string>("张三",20);//参数二采用了默认值int
}
成员函数创建时期
- 成员函数在调用时才被创建
可能出现的情况:模板类中定义的函数,在模板被实现后无法被调用
换种说法,若出现该类问题,不会在编译时被编译器查出
类模板对象作函数参数
传入方式:
- 指定传入的类型 直接显示对象的数据类型
1
2
3
4
5
6
7
8
9
10
11template<class T1,class T2>
class Person
{
T1 name;
T2 age;
}
void showPerson(Person<string,int>&p)
{
p.show();
} - 参数模板化 将对象中的参数变为模板进行传递
1
2
3
4
5template<class T1,class T2>
void showPerson(Person<T1,T2>&p)
{
p.show();
} - 整个类模板化 将这个对象类型模板化进行传递
1
2
3
4
5template<class T>
void showPerson(T &p)
{
p.showperson();
}
类模板与继承
- 当子类继承的父类是一个类模板时,子类在声明时需要指定T的类型
1
2
3
4
5
6
7
8
9template<class T>
class Base
{
T m;
}
class Son:public Base<int>
{
//可以正常写了
} - 如果不指定,编译器无法给子类分配内存
- 如果想灵活指定T的类型,子类也需要为类模板
1
2
3
4
5
6
7
8
9
10
11template<class T>
class Base
{
T m;
}
template<class T1,class T2>
class Son:public Base<T2>
{
T1 m;
//可以正常写了
}
类模板成员函数的类外实现
1 | template<class T1,class T2> |
类模板分文件编写
问题:成员函数在调用阶段生成,导致分文件编写时无法被链接
解决方案:
- 直接包含cpp源文件(会通过.cpp的头文件自动包含.h文件)
- 将声明和实现写到同一个文件中,并更改后缀名为hpp(约定)
类模板与友元
类内实现:直接在类内声明友元即可
类外实现:提前让编译器知道全局函数存在
1 | template<class T1,class T2> |