浅记C++
- 对油管大佬Cherno的Cpp教程记录的笔记还加入了黑马的内容进行补充(持续跟新)
- 1 C++变量
- 2 C++函数
- 3 C++头文件
- 4 回顾 if else;循环
- 5 指针(原始指针)
- 6 引用
- 7 C++类
- 8 类和结构体的区别
- 9 静态static
- 10 局部静态
- 11 枚举
- 12 构造函数
- 13 析构函数
- 14 继承
- 15 虚函数
- 16 C++接口(纯虚函数)
- 17 C++可见性
- 18 数组
- 19 字符串
- 20. C++字符串字面量
- 21. C++中CONST
- 22 C++mutable关键字
- 23 构造函数初始化列表
- 24 三元运算符
- 25 创建并初始化c++对象
- 26 new关键字
- 27C++隐式转换与explicit关键字
- 28.C++运算符(操作符)及其重载
- 29. C++对象模型和this指针
- 30. 内存分区模型
- 31. C++的智能指针 (感觉自己一知半解)
- 32. 函数默认参数
- 33. 函数占位参数
- 34. 函数重载
- 35. 类和对象(黑马视频回顾一下)
对油管大佬Cherno的Cpp教程记录的笔记还加入了黑马的内容进行补充(持续跟新)
定义:这个函数到底是什么(有函数体)
申明:这个符号,函数是存在的(无函数体)
编译:指将源c++文件转换到实际的可执行文件
链接:主要焦点是找到每个符号和函数在哪,并把他们连接起来。每个文件被编译成一个单独的目标文件,一个翻译单元,他们之间没有关系,没有交互。所以需要链接过程
注意对于VS报错,c是编译错误,LNK是链接错误
1 C++变量
1.不同变量类似之间的唯一区别是在c++中的大小
2.不同编译器的变量内存大小不同
3.怎么看变量类型的范围:int为例。int四个字节,一个字节8比特,就是32比特,其中有一位表示为-,所以int为-2^31 - 2^31
4.怎么区分float和double, float 4个字节,数字后面带f/F,如5.2F;double 8个字节
5.一个数据究竟多大? sizeof操作符
2 C++函数
防止代码重复,一个任务做了多次就可u函数写了
3 C++头文件
1.头文件两种写法 <>其他文件夹下,只用于编译器包含路径
""适用一切文件下,但只用在相对路劲
2.c文件标准库的文件带.h; c++不带.h
4 回顾 if else;循环
1.对于指针,有个技巧判断指针是不是0/nullptr/NULL,可以放到if里
2.else if不是c++的关键字,而是先else 在if
3.continue会跳过当前循环,开始下一个循环
5 指针(原始指针)
1.指针是一个整数,一种存储内存地址的数字
2.*ptr = 10,这样就是逆向使用指针了,将指针指向的内容改成了10
3.怎么用指针分配一定的内存
char* buffer = new char[8]; #通过new创造8字节的内容,并且指向内存开始的指针
memset(buffer, 0, 8); #memse用指定的数据0填充一个内存块,接受一个指针(内存开始的指针)
通过new关键字申请了堆内存,完成后应该删除数据
delete[] buffer;
4.双指针就是一个指针指向另外一个指针内存地址
5.1 空指针和野指针
- 空指针:指针变量指向内存中编号为0的空间
用途:初始化指针变量,也就是指针不知道指向什么的时候可以赋为空指针。
注意:空指针指向的内存是不可以访问的。内存编号0 ~255为系统占用内存,不允许用户访问
指针变量p指向内存地址编号为0的空间
int * p = NULL;
2.野指针:指针变量指向非法的内存空间
6 引用
1.根本上引用只是指针的伪装,在c++内部实现是一个指针常量
int& ref = a转换为 int* const ref = &a
2.引用本身并不是新的变量,只能引用已经存在的变量
3.int&,这样写&只是变量的类型,不是取地址,所以这是引用
4.引用可以节省内存分配
int& ref = a; #只是将a创造成别名ref,ref不是真正的变量,实际上不存在,这段代码只能得到a的变量,修改ref就是修改a,ref=a
6.1 引用注意事项
引用必须在合法的内存空间 要么在栈区 要么在堆区
引用必须初始化
引用在初始化后,不可以改变
int &c; 错误,引用必须初始化
int &c = a; 一旦初始化后,就不可以更改
c = b; 这是赋值操作,不是更改引用
6.2 引用做函数参数
作用:函数传参时,可以利用引用的技术让形参修饰实参,形参变了,实参也就变了
优点:可以简化指针修改实参
为了修改形参,实参不变:
在函数形参列表中,可以加const修饰形参,防止形参改变实参
void 函数名(const int& 变量) {}
6.3 引用做函数返回值
作用:引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用,因为局部变量在栈区,函数执行完会释放掉
用法:函数调用作为左值
7 C++类
1.简单来说,类只是对数据和功能组合在一起的一种方式
2.class name(名字唯一)
3.由类类型构成的变量称为对象,新的对象变量称为实列
# Player就是被称为对象,public是可以在类之外任何地方访问这些变量 类中的函数被称为方法如move
class Player {
public:
int x, y;
int speed;
void move(int a, int b){
x += a * speed;
y += b * speed;
}
};
8 类和结构体的区别
1.区别:class默认private,struct默认public
2.struct是为了让C++向后兼容C。 #define struct class
3.如何选这两个
若只包含一些变量结构或POD(plain old data)时,选用struct。例如数学中的向量类。
建议:struct一般不用继承,继承是一种增加另一层次复杂性的东西,结构体是数据的结构
struct Vec2{
float x, y;
void Add(const Vec2& other){
x += other.x;
y += other.y;
}
};
若要实现很多功能的类,则选用class
9 静态static
static关键字两种用法
1.在类或结构体外部使用static关键字
1)这意味着你定义的函数和变量只对它的声明所在的cpp文件(编译单元)是“可见”的。换句话说此时static修饰的符号,(在link的时候)它只对定义它的翻译单元(.obj)可见
2)如果不用static定义全局变量,在别的翻译单元可以用extern int a,这样就能在别的文件寻找a
3)如过用static定义了,那么用extern没用
4)全局变量的话尽量用static定义,不定义的话这个变量是完全全局的,可以在任何地方使用,可能会导致bug
5)再次强调,要让全局函数和变量标记为静态的,除非真的需要他们跨越翻译
2.在类或结构体内部使用static关键字
静态成员变量在编译时存储在静态存储区,即定义过程应该在编译时完成,因此一定要在类外进行定义,但可以不初始化。 静态成员变量是所有实例共享的,在类中进行了声明,
类型 类名::变量名;
1)此时表示这部分内存(static变量)是这个类的所有实例共享的。即:该静态变量在类中创建的所有实例中,静态变量只有一个实例。一个改变就改变所有。
比如一个entity类,有很多个entity实例,若其中一个实例更改了这个static变量,它会在所有实例中反映这个变化。因为相同的静态变量指向的是相同的内存,指向同一个地方。正因如此,通过类实例来引用静态变量是没有意义的。因为这就像类的全局实例。
2)静态方法不能访问非静态变量,因为静态方法没有类实例,本质上你在类里写的每个非静态方法都会获得当前的类实例作为参数,这就是类的幕后工作,你看不到而已,通过隐藏参数发挥作用,静态方法无法获得当前类实例当作参数
因此静态方法和在类外部编写的方法是一样的。
struct Entity{
int x;
static void print() {
cout << x << endl; // 报错,不能访问到非静态变量x
}
};
static void print(){
cout << x << endl; // 报错,x是什么?没被定义。
}
本质上就是一个类的非静态方法在编译时的真正样子
static void print(Entity e){
cout << e.x << endl; // 成功运行
}
10 局部静态
在局部作用域中可以使用static来声明一个变量,这和前两种有所不同。这一种情况需要考虑变量的生命周期和作用域。
生命周期:变量实际存在的时间;
作用域:指可以访问变量的范围。
静态局部(local static)变量允许我们声明一个变量,它的生命周期基本相当于整个程序的生命周期,然而它的作用范围被限制在这个函数作用域内。
#include <iostream>
void Function(){
static int i = 0; //这句的意思是当我第一次调用这个函数时它的值被初始化为0,后续调用不会再创建一个新的变量,这个就是局部静态
i++; //如果上一行没有static结果会是每次调用这个函数i的值被设为0,然后i自增1向控制台输出1
std::cout << i << std::endl;
}
int main(){
Function();
Function();
Function();
std::cin.get()
}
输出1 2 3
这其实就如同在函数外声明一个全局变量:
#include <iostream>
int i = 0;//声明一个全局变量
void Function(){
i++;
std::cout << i << std::endl;
}
int main(){
Function();
Function();
Function(); //输出 1 2 3
std::cin.get()
}
但是这种问题是:可以在任何地方访问到变量 i
int main()
{
Function();
i = 10; // 可以在函数之间改变i的值
Function();
Function(); //输出 1 11 12
std::cin.get()
}
11 枚举
1.enum是enumeration的缩写。基本上它就是一个数值集合。不管怎么说,这里面的数值只能是整数。
2.定义枚举类型的主要目的:增加程序的可读性
3.枚举变的名字一般以大写字母开头(非必需)
4.默认情况下,编译器设置第一个 枚举变量值为 0,下一个为 1,以此类推(也可以手动给每个枚举量赋值),且 未被初始化的枚举值的值默认将比其前面的枚举值大1 )
5.枚举量的值可以相同
6.枚举类型所使用的类型默认为int类型,也可指定其他类型 ,如 unsigned char
enum example { //声明example为新的数据类型,称为枚举(enumeration);
Aa, Bb, Cc //声明Aa, Bb, Cc等为符号常量,通常称之为枚举量,其值默认分别为0,1,2
};
enum example {
Aa = 1, Bb, Cc = 1,Dd, Ee //1 2 1 2 3 未被初始化的枚举值的值默认将比其前面的枚举值大1。
};
enum example : unsigned char //将类型指定成unsigned char,枚举变量变成了8位整型,减少内存使用。默认是int
{
Aa, Bb = 10, Cc
};
enum example : float //ERROR!枚举量必须是一个整数,float不是整数(double也不行)。
{
Aa, Bb = 10, Cc
};
**枚举量的定义:**可利用新的枚举类型example声明这种类型的变量 example Dd,可以在定义枚举类型时定义枚举变量
enum example
{
Aa = 0, Bb, Cc
};
example ex;
enum example
{
Aa, Bb, Cc
}Dd;
对于枚举,只定义了赋值运算符,没有为枚举定义算术运算 ,但能参与其他类型变量的运算
Aa++; //非法!
Dd = Aa + Cc //非法!
int a = 1 + Aa //Ok,编译器会自动把枚举量转换为int类型。
12 构造函数
构造函数是一种特殊的类型方法,他在每次实例化对象时运行,构造函数最重要的作用就是初始化类
1.构造函数没有返回类型
2.构造函数的命名必须和类名一样
注:C++的基本数据类型,如float double等,会自动初始化为0,C++不会,只能手动初始化
class Entity {
public:
int x, y;
Entity(){//不带参数
...
...
}
Entity(int x1, int y1) : x(x1), y(y1) {
} //带参数,用来初始化x和y,成员初始化列表
void print()
{
std::cout << x << ',' << y << std::endl;
}
};
如果你不指定构造函数,你仍然有一个构造函数,这叫做默认构造函数,是默认就有的。但是,我们仍然可以删除该默认构造函数:
class Log{
public:
Log() = delete; //删除默认构造函数
......
}
13 析构函数
1.析构函数是在你销毁一个对象的时候运行。 卸载变量,清理使用过的内存
2.析构函数同时适用于栈和堆分配的内存。
1)因此如果你用new关键字创建一个对象(存在于堆上),然后你调用delete,析构函数就会被调用。
2)如果你只有基于栈的对象,当跳出作用域的时候这个对象会被删除,所以这时侯析构函数也会被调用
3.构造函数和析构函数在声明和定义的唯一区别就是放在析构函数前面的波形符(~)
4.析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。
5.因为这是栈分配的,当超出作用域时的时候析构函数就会被调用,会自动销毁
为什么要用析构函数?
如果在构造函数中调用了特定的初始化代码,必须在析构函数中删除这些,不这样做就会导致内存泄漏。
比如在堆上分配对象。如果在堆上手动分配了任何类型的内存,这需要要手动清理
14 继承
1.继承如此有用的主要原因是可以帮助我们避免代码重复,代码重复指本质相,只是略微不同
2.当你创建了一个子类,它会包含父类的一切。
3.继承给我们提供了这样的一种方式:把一系列类的所有通用的代码(功能)放到基类
在定义一个新的类 B 时,如果该类与某个已有的类 A 相似(指的是 B 拥有 A 的全部特点),那么就可以把 A 作为一个基类,而把B作为基类的一个派生类(也称子类)。
4.派生类是通过对基类进行修改和扩充得到的,在派生类中,可以扩充新的成员变量和成员函数。
5.派生类拥有基类的全部成员函数和成员变量,不论是private、protected、public。需要注意的是:在派生类的各个成员函数中,不能访问基类的private成员。
class 派生类名:public 基类名
{
};
15 虚函数
虚函数允许在子类中重写方法
只需要知道,如果想重写一个函数,就必须将基类中的基函数标记为虚函数
注意:std::string 是用来定义一个返回字符串对象的函数
std::string GetName() {return “Entity”;} 这里就是定义一个返回Entity的GetName函数
虚函数的例子,通常有三步。
第一步,定义基类,声明基类函数为 virtual 的。
第二步,定义派生类(继承基类),派生类实现了定义在基类的 virtual 函数。
第三步,声明基类指针,并指向派生类,调用virtual函数,此时虽然是基类指针,但调用的是派生类实现的基类virtual 函数。
为什么用虚函数?
看下面代码:
//基类
class Entity
{
public:
std::string GetName() {return "Entity";}
};
//派生类
class Player : public Entity
{
private:
std::string m_Name;
public:
Player(const std::string& name):m_Name (name) {} //构造函数
std::string GetName() {return m_Name;}
};
int main(){
Entity* e = new Entity();//不用手动删除,因为程序会终止(对象被自动删除)
std::cout << e->GetName() << std::endl;
Player* p = new Player("cherno");
std::cout << p->GetName() << std::endl;
}
---------------------------------
//输出
Entity
cherno
如果我引用这个Player并把它当成Entity类型,就会出现问题,例如:
int main(){
Entity* e = new Entity();//不用手动删除,因为程序会终止(对象被自动删除)
std::cout << e->GetName() << std::endl;
Player* p = new Player("cherno");
std::cout << p->GetName() << std::endl;
Entity* entity = p; //p是一个Player类型的指针,它是一个Player,但是我把它指向了一个Entity,记住虽然指向e,但还是p
std::cout << entity->GetName() << std::endl;
}
Entity
cherno
Entity //运行代码,你会看见打印出了"Entity",但是我们希望的是打印cherno
举一个更加清晰的例子
//基类
class Entity{
public:
std::string GetName() {return "Entity";}
};
//派生类
class Player : public Entity{
private:
std::string m_Name;
public:
Player(const std::string& name):m_Name (name) {} //构造函数
std::string GetName() {return m_Name;}
};
void printName(Entity* entity){
std::cout << entity -> GetName() << std::endl;
}
int main(){
Entity* e = new Entity();
printName(e); //我们这儿做的就是调用entity的GetName函数,我们希望这个GetName作用于Entity
//就是把上面的e->GetName()弄成一个函数
Player* p = new Player("cherno");
printName(p); //printName(Entity* entity),相当于Entity* entity = p
//没有报错是因为Player也是 Entity类型。同样我们希望这个GetName作用于Player
}
输出
Entity
Entity
两次输出都是Entity,原因在于如果我们在类中正常声明函数或方法,当调用这个方法的时候,它总是会去调用属于这个类型的方法,而void printName(Entity* entity);参数类型是Entity*,意味着它会调用Entity内部的GetName函数,它只会在Entity的内部寻找和调用GetName.
但是我们希望C++能意识到,在这里我们传入的其实是一个Player,所以请调用Player的GetName。此时就需要使用虚函数了。
个人理解,虚函数就是区分子类和父类的同名函数
所以,简单来说,你需要知道的就是如果你想重写一个函数,你么你必须要把基类中的原函数设置为虚函数
//基类
class Entity
{
public:
virtual std::string GetName() {return "Entity";} //第一步,定义基类,声明基类函数为 virtual的。
};
//派生类
class Player : public Entity
{
private:
std::string m_Name;
public:
Player(const std::string& name):m_Name (name) {}
//第二步,定义派生类(继承基类),派生类实现了定义在基类的 virtual 函数。
std::string GetName() override {return m_Name;} //C++11新标准允许给被重写的函数用"override"关键字标记,增强代码可读性。
};
void printName(Entity* entity){
std::cout << entity -> GetName() << std::endl;
}
int main(){
Entity* e = new Entity();
printName(e);
Entity* p = new Player("cherno"); //第三步,声明基类指针p,并指向派生类,调用`virtual`函数,
//此时虽然是基类指针,但调用的是派生类实现的基类virtual函数。
Player* p = new Player("cherno"); //也能这样
printName(p);
}
16 C++接口(纯虚函数)
1.C++中的纯虚函数本质上与其他语言(i如Java或C#)中的抽象方法或接口相同。
2.纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。
3.在某些情况下,在基类提供默认实现是没有意义的,实际上可能强制子类,为特定的函数提供自己的定义,
在面向对象程序设计中,创建一个只包含未实现方法组成的类。然后强制子类实现他们。这通常被称为接口。因此,类中的接口只包含未实现的方法,作为模板。并且由于此接口类实际上不包含方法实现,所以我们无法实例化这个类。
4.声明方法: 在基类中纯虚函数的方法的后面加 =0
virtual std::string GetName() = 0;
示例
class Printable{ //接口。其实就是个类。之所以称它为接口,只是因为它有一个纯虚函数,仅此而已。
public:
virtual std::string GetClassName()= 0;
};
//基类
class Entity : public Printable
{
public:
std::string GetClassName() override {return "Entity";} //实现接口的纯虚函数
};
//派生类
class Player : public Entity //因为Player已经是Entity(已有接口),所以Player不用去实现Printable接口
{
private:
std::string m_Name;
public:
Player(const std::string& name):m_Name (name) {}
std::string GetClassName() override {return "Player";} //实现接口的纯虚函数
};
void Print(Printable* obj){ //我们需要某种类型能提供GetClassName函数,这就是接口。所有的打印都来自于这个Print函数,它接受printable对象,它不关心实际是什么类
std::cout <<obj ->GetClassName() << std::endl;
}
int main(){
Entity* e = new Entity();
Player* p = new Player("cherno");
Print(e);
Print(p);
}
//输出:
Entity
Player
上例中,如果Player不是Entity的话,就要添加接口,如下
class Player : public OtherClass,Printable //加逗号,添加接口Printable
{
....
std::string GetName() override {return m_Name;}
};
17 C++可见性
1.可见性是一个属于面向对象编程的概念,它指的是类的某些成员或方法实际上是否可见。可见性是指:谁能看到它们,谁能调用它们,谁能使用它们,所有这些东西。
2.C++中有三个基础的可见修饰符(访问修饰符):private、protected、public
private:只有自己的类和它的**友元(friend)**才能访问(继承的子类也不行,友元的意思就是可以允许你访问这个类的私有成员)。
protected:这个类以及它的所有派生类都可以访问到这些成员。(但在main函数中new一个类就不可见,这其实是因为main函数不是类的函数,对main函数是不可访问的)
public:谁都可见。
18 数组
1.回顾:数组名是一个指针,可以指向整个数组(数组的第一个数据)
int main()
{
int example[5];
int* ptr = example;
for (int i = 0; i< 5;i++) //5个元素全部设置为2
example[i] = 2;
example[2] = 5; //第三个元素设置为5
*(ptr + 2) = 6; //第三个元素设置为6。因为它会根据数据类型来计算实际的字节数,所以在这里因为这个指针是整形指针所以会是加上2乘以4,因为每个整形是4字节
*(int*)((char*)ptr + 8) = 7; //第三个元素设置为7。因为每个char只占一个字节
//char*将整形指针强制转换成char型,加8就是往后移动8位,刚好是int数组的第三个位置
std::cin.get();
}
2.new关键字可以创造一个对象(实例),也能创造一数组
栈数组int example[5]; 堆数组int* another = new int[5];
int main()
{
int example[5]; //这个是创建在栈上的,它会在跳出这个作用域(结束的})时被销毁
for (int i = 0; i< 5;i++) //5个元素全部设置为2
example[i] = 2;
int* another = new int[5];//这行代码和之前的是同一个意思,但是它们的生存期是不同的. new创造数组会一直存在,直到自己删除
//因为这个是创建在堆上的,实际上它会一直存活到直到我们把它销毁或者程序结束。
// 所以你需要用delete关键字来删除它。
for (int i = 0; i< 5;i++) //5个元素全部设置为2
another[i] = 2;
delete[] another; //delete[] 因为数组是通过【】创造内存,所以需要delete【】删除内存
std::cin.get();
}
上述的两个数组在内存上看都是一样的,元素都是5个2;
那为什么要使用new关键字来动态分配,而不是在栈上创建它们呢?最大的原因是因为生存期,因为new分配的内存,会一直存在,直到你手动删除它。
如果你有个函数要返回新创建的数组,那么你必须要使用new来分配,除非你传入一个数组的地址参数 。
3.间接寻址
new创建数组可能会导致间接寻址
意思是,有个指针,指针指向另一个保存着我们实际数组的内存块(p-> p-> array),这会产生一些内存碎片和缓存丢失。为了避免这种情况,应该在栈上创建数组
4.C++11中的std:array
这是一个内置数据结构,定义在C++11的标准库中。很多人喜欢用它来代替这里的原生数组,因为他有很多优点,它有边界检查,有记录数组的大小
实际上我们没有办法计算原生数组的大小,但可以通过一些办法知道大小(例如因为当你删除这个数组时,编译器要知道实际上需要释放多少内存)。
4.1 计算原生数组大小
创建一个栈数组,你不知道他的实际大小,因为它是在栈上分配的
int a[5]; //栈数组
如果你想知道有多少个元素,可以用下面方法,得到5
sizeof(a); //20bytes
int count = sizeof(a) / sizeof(int); //5
但是如果你用堆数组
int* example = new int[5]; //堆数组
int count = sizeof(example) / sizeof(int); //1。得到的实际上是一个整形指针的大小(int* example),就是4字节,4/4就是1。
所以只能在栈分配的数组上用这个技巧,但是你真的不能相信这个方法,所以你要做的就是自己维护数组的大小。
如何维护呢?方法有两个
方法一:
class Entity
{
public:
static constexpr const int size = 5;//在栈中为数组申请内存时,它必须是一个编译时就需要知道的常量。
//constexpr可省略,但类中的常量表达式必须时静态的
int example[size]; //此时为栈数组,
Entity()
{
for (int i = 0; i<size;i++)
example[i] = 2;
}
};
方法二:std:array
include <array> //添加头文件
class Entity
{
public:
std::array<int,5> another; //使用std::array
Entity()
{
for (int i = 0; i< another.size();i++) //调用another.size()
example[i] = 2;
}
};
这个方法会安全一些。
19 字符串
1…字符串实际上是字符数组
2.const char* name = "cherno";
const就不能更改字符串了
3.C++中默认的双引号就是一个字符数组const char,并且*末尾会补’\0’ (空终止符),
字符串最后一位就是0(空终止符)
而cout会输出直到’\0’就终止。
char* name = "cherno" //报错,因为C++中默认的双引号就是一个字符数组const char*
char name[3] = {'l','i','u'};//报错,缺少空终止符
char name[3] = {'l','i','u',0};//正确
char name[3] = {'l','i','u','\0'};//正确,因为ascii码'\0'就是null
4.C++标准库里有个类叫string,实际上还有一个模板类BasicString。std::string 本质上就是这个BasicString的char作为模板参数的模板类实例。叫模板特化,就是把char作为模板类BasicString的模板参数,意味着char就是每个字符背后的的数据类型。
在C++中使用字符串时你应该使用std::string
5.std::string本质上它就是一个char*,一个字符数组和一些操作这个字符数组的函数
6. string有个接受参数为char指针或者const char指针的构造函数。在C++中用双引号来定义字符串一个或者多个单词时,它其实就是const char数组,而不是char数组。,不是真正的字符串
使用:
std::string name = "Cherno"; //string类中包含了很多功能,比如name.size(),计算字符串大小
7.追加字符串
std::string name = "Cherno" + "hello!";//ERROR!
将两个const char数组相加,,双引号里包含的内容是const char数组,它不是真正的字符串g;不能将两个指针或者两个数组加在一起,它不是这么工作的。
所以如果你想这么做,要么就是把它们分成多行
std::string name = "Cherno"";
name += "hello! //OK
这样做是在将一个指针加到了字符串name上了,然后**+=这个操作符在string类中被重载了**,所以可以支持这么操作。
或者经常做的是显式地调用string构造函数将其中一个传入string构造函数中,相当于你在创建一个字符串,然后附加这个给他。
std::string name = std::string("Cherno") + "hello!";//OK
std::string name = "Cherno" + std::string("hello!)";//OK
20. C++字符串字面量
1.字符串字面量是在双引号之间的一串字符,实际上是数组或者指针
2.字符串字面量是存储在内存的只读部分的,不可对只读内存进行写操作
3.C++11以后,默认为const char*,否则会报错
const char* name = "cherno"; //Ok!
name[2] = 'a'; //ERROR!const不可修改
//如果你真的想要修改这个字符串,你只需要把类型定义为一个数组而不是指针
char name[] = "cherno"; //Ok!
name[2] = 'a'; //ok
4.从C++11开始,有些编译器比如Clang,实际上只允许你编译const char*, 如果你想从一个字符串字面量编译char,你必须手动将他转换成char*
char* name = (char*)"cherno"; //Ok!
name[2] = 'a'; //OK
5.其他字符串
char是一个字节的字符,char16_t是两个字节的16个比特的字符(utf16),char32_t是32比特4字节的字符(utf32),const char就是utf8. 那么wchar_t也是两个字节,和char16_t的区别是什么呢?事实上宽字符的大小,实际上是由编译器决定的,可能是一个字节也可能是两个字节也可能是4个字节,实际应用中通常不是2个就是4个(Windows是2个字节,Linux是4个字节),所以这是一个变动的值。如果要两个字节就用char16_t,它总是16个比特的。
const char* name = "";
const wchar_t* name2 = L"";
const char16_t* name3 = u"";
const char32_t* name4 = U"";
const char* name5 = u8"";
6.c++14的string_literals库
在这个库中能简便代码
#include <iostream>
#include <string>
int main()
{
using namespace std::string_literals;
std::string name0 = "hbh"s + " hello";
std::cin.get();
}
string_literals中定义了很多方便的东西**,这里字符串字面量末尾加s,可以看到实际上是一个操作符函数,它返回标准字符串对象(std::string)**
然后我们就还能方便地这样写等等:std::wstring name0 = L"hbh"s + L" hello";
string_literals也可以忽略转义字符
int main(){
using namespace std::string_literals;
const char* example =R"(line1
line2
line3
line4)"
std::cin.get();
21. C++中CONST
1.const被cherno称为伪关键字,因为它在改变生成代码方面做不了什么。有点像类和结构体的可见性,这是一个机制,让代码更加干净
2.const是一个承诺,承诺一些东西是不变的,你是否遵守诺言取决于你自己。我们要保持const是因为这个承诺实际上可以简化很多代码。
3.const指针的用法
适用于指针和指针指向的内容
const int* a 和 int const* a 一样
const int* a = new int; //创造一个堆指针
*a = 2; //error! 不能再去修改指针指向的内容了。
a =(int*)&Age //可以改变指针指向的地址
int* const
可以改变指针指向的内容,不能再去修改指针指向的地址
int* const a = new int;
*a = 2; //ok
a =(int*)&Age //error
个人理解 :const首先优先左边;其次用于右边
const int* a 就是可以看出用于int 所以不能修改指针指向的内容,但是可以修改指针本身地址
int* const a 就是作用int* 所以不能修改int 型指针本身地址,但是可以修改指针指向的内容
const int* const 既不可以改变指针指向的内容,也不能再去修改指针指向的地址
4.在类和方法中的const
const的第三种用法,他和变量没有关系,而是用在方法名的后面( 只有类才有这样的写法 )
这意味这这个方法不会修改任何实际的类,不能修改类的成员变量
class Entity
{
private:
int m_x,m_y;
public:
int Getx() const //const的第三种用法,他和变量没有关系,而是用在方法名的后面
{
return m_x; //不能修改类的成员变量
m_x = 2; //ERROR!
这里本质是 this->m_x = 2;
this指针的本质是 指针常量 指针的指向是不可以修改的
去掉const 就能修改this指向的值,也就是m_x
}
void Setx(int a)
{
m_x = a; //ok
}
};
void PrintEntity(const Entity& e) //const Entity调用const函数
{
std::cout << e.Getx() << std::endl;
}
int main()
{
Entity e;
}
一般写两个Getx版本,一个有const一个没有,然后上面面这个传const Entity&的方法就会调用const的GetX版本。Entity& e就是调用没有const的版本。
所以,我们把**成员方法标记为const是因为如果我们真的有一些const Entity对象,我们可以调用const方法。**如果没有const方法,那const Entity&对象就掉用不了该方法。
在const函数中, 如果要修改别的变量,可以用关键字mutable:
class Entity
{
private:
int m_x,m_y;
mutable var;
public:
int Getx() const
{
var = 2; //ok mutable var
return m_x; //不能修改类的成员变量
m_x = 2; //ERROR!
}
};
22 C++mutable关键字
mutable 可变的,易变的
mutable有两种不同的用途:
1.与const一起用(最主要的用法),用mutable类似可以翻转,改变const
class Entity
{
private:
std::string m_Name;
mutable int m_DebugCount = 0;
public:
const std::string& Getname() const //第一个const代表不能修改字符串内容,只读
//所以就用&,直接引用,不用担心浪费内存
//第二个const代表这个函数不能修改类的成员变量
{
m_DebugCount++; //因为是mutable型,所以可以修改
return m_Name;
}
};
2.用在lambda表达式中,或者同时包含这两种情况
Lambda表达式是现代C++在C ++ 11和更高版本中的一个新的语法糖 ,在C++11、C++14、C++17和C++20中Lambda表达的内容还在不断更新。 lambda表达式(也称为lambda函数)是在调用或作为函数参数传递的位置处定义匿名函数对象的便捷方法。
Lambda有很多叫法,有Lambda表达式、Lambda函数、匿名函数,
ISO C ++标准官网一个简单的lambda 表示式实例
#include <algorithm>
#include <cmath>
void abssort(float* x, unsigned n) {
std::sort(x, x + n,
// Lambda expression begins
[](float a, float b) {
return (std::abs(a) < std::abs(b));
} // end of lambda expression
);
}
1.捕获列表。在C ++规范中也称为Lambda导入器, 捕获列表总是出现在Lambda函数的开始处。[]是Lambda引出符。编译器根据该引出符判断接下来的代码是否是Lambda函数,捕获列表能够捕捉上下文中的变量以供Lambda函数使用。
[]表示不捕获任何变量
[var]表示值传递方式捕获变量var
[=]表示值传递方式捕获所有父作用域的变量(包括this)
[&var]表示引用传递捕捉变量var
[&]表示引用传递方式捕捉所有父作用域的变量(包括this)
[this]表示值传递方式捕捉当前的this指针
2.参数列表。与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号“()”一起省略。
3.可变规格。mutable修饰符, 默认情况下Lambda函数总是一个const函数,mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略(即使参数为空)。
4.异常说明。用于Lamdba表达式内部函数抛出异常。
5.返回类型。 追踪返回类型形式声明函数的返回类型。我们可以在不需要返回值的时候也可以连同符号”->”一起省略。此外,在返回类型明确的情况下,也可以省略该部分,让编译器对返回类型进行推导。
6.lambda函数体。内容与普通函数一样,不过除了可以使用参数之外,还可以使用所有捕获的变量。
后面具体将具体说明。
23 构造函数初始化列表
这是我们构造函数初始化类成员(变量)的一种方式 。
因此当编写一个类,并向该类添加成员,需要通过某种方式对这些成员初始化
注意:在成员初始化列表里需要按成员变量定义的顺序写。这很重要,因为不管你怎么写初始化列表,它都会按照定义类的顺序进行初始化。
使用成员初始化列表的原因:代码风格简洁 避免性能浪费
有两种方法可以在构造函数中初始化类成员
class Entity{
private:
std::string m_Name;
public:
Entity() //默认构造函数
{
m_Name = "Unknow";
}
Entity(const std::string& name)
{
m_Name = name;
}
};
方法二:初始化成员列表
class Entity
{
private:
std::string m_Name;
int m_Score;
public:
Entity() : m_Name("Unknow"), m_Score(0)
{
}
Entity(const std::string& name,int n) :m_Name(name), m_Score(100)
{
}
const std::string& GetName() const { return m_Name; };
const int& GetScore() const { return m_Score; };
};
int main()
{
Entity e0;
Entity e1("lk",50);
std::cout << e0.GetName() <<e0.GetScore() << std::endl;
std::cout << e1.GetName() <<e1.GetScore()<<std::endl;
}
24 三元运算符
格式:
条件表达式 ? 表达式1 : 表达式2;
语义:如果“条件表达式”为true,则整个表达式的值就是表达式1,忽略表达式2;
如果“条件表达式”为false,则整个表达式的值就是表达式2,等价于if/else语句。
实际上只是if的语法糖。
作用: - 代码更简洁 - 速度更快一点
尽量不对三元操作符进行嵌套
25 创建并初始化c++对象
基本上,当我们编写了一个类并且到了我们实际开始使用该类的时候,就需要实例化它(除非它是完全静态的类)
实例化类有两种选择,这两种选择的区别是内存来自哪里,我们的对象实际上会创建在哪里。
应用程序会把内存分为两个主要部分:堆和栈。还有其他部分,比如源代码部分,此时它是机器码。
栈分配
// 栈中创建,栈对象有一个自动的生存期,由他声明的地方作用域决定的,只要超出作用域,栈会弹出作用域里面的东西
// 作用域不一定是函数 可以是if语句,甚至是空的{}
Entity entity;
Entity entity("lk");
回顾:int a[5]; //栈数组
什么时候栈分配?几乎任何时候,因为在C++中这是初始化对象最快的方式和最受管控的方式。
什么时候不栈分配? 如果创建的对象太大,可能没有足够的空间在栈上分配,应为栈通常非常小
或是需要显示地控制对象的生存期,那就需要堆上创建
堆分配
// 堆中创建
Entity* entity = new Entity("lk");
delete entity; //清除
当我们调用new Entity时,实际发生的就是我们在堆上分配了内存,我们调用了构造函数,
然后这个new Entity实际上会返回一个Entity指针,它返回了这个entity在堆上被分配的内存地址,
这就是为什么我们要声明成Entity*类型。
26 new关键字
new的主要目的是分配内存,具体来说就是在堆上分配内存。
如果你用new和[]来分配数组,那么也用delete[]。
new主要就是找到一个满足我们需求的足够大的内存块,然后返回一个指向那个内存地址的指针。
int* a = new int; //这就是一个在堆上分配的4字节的整数,这个a存储的就是他的内存地址.
int* b = new int[50];//在堆上需要200字节的内存。4*50
delete a;
delete[] b; 分配了数组所以要这样
//在堆上分配Entity类
Entity* e = new Entity();
Entity* e = new Entity;//或者这我们不需要使用括号,因为他有默认构造函数。
Entity* e0 = new Entity[50]; //如果我们想要一个Entity数组,我们可以这样加上方括号,在这个数组里,你会在内存中得到50个连续的Entity
delete e;
delete[] e0;
在new类时,该关键字做了两件事
分配内存 调用构造函数
Entity* e = new Entity();//1.分配内存 2.调用构造函数
Entity* e = (Entity*)malloc(sizeof(Entity);//仅仅只是分配内存**然后给我们一个指向那个内存的指针
//这两行代码之间仅有的区别就是第一行代码new调用了Entity的构造函数
//在c++中建议不要这样写
delete e;//new了,必须要手动清除
new 是一个操作符,就像加、减、等于一样。它是一个操作符,这意味着你可以重载这个操作符,并改变它的行为。
通常调用new会调用隐藏在里面的C函数malloc,但是malloc仅仅只是分配内存然后给我们一个指向那个内存的指针,而new不但分配内存,还会调用构造函数。同样,delete则会调用destructor析构函数。
new支持一种叫placement new的用法,这决定了内存来自哪里, 所以你并没有真正的分配内存。在这种情况下,你只需要调用构造函数,并在一个特定的内存地址中初始化你的Entity,可以通过new()然后指定内存地址,例如:
int* b = new int[50]; 已经分配了内存
Entity* entity = new(b) Entity(); 这里就不用分配内存了,在特定的内存中初始化实列
27C++隐式转换与explicit关键字
1.c++允许编译器对代码执行一次隐式转换
实例
#include <iostream>
class Entity
{
private:
std::string m_Name;
int m_Age;
public:
Entity(const std::string& name)
: m_Name(name), m_Age(-1) {}
Entity(int age)
: m_Name("Unknown"), m_Age(age) {}
};
void PrintEntity(const Entity& entity){
//用来打印
}
int main()
{
Entity a("Cherno");
Entity b(22); //正常赋值写法
Entity a = "Cherno"; //将const char数组转换成entity
Entity b = 22; //隐式转换,将22转换成 Entity类型 有Entity的构造函数接受一个整数的参数
PrintEntity(22); //这里是对的,22可以隐式转换成一个Entity
PrintEntity("Cherno"); //错
std::cin.get();
}
PrintEntity(22) 对。因为Entity类中有一个Entity(int age)构造函数,因此可以调用这个构造函数,然后把22作为他的唯一参数,就可以创建一个Entity对象。
PrintEntity(“Cherno”); //错 原因是只能进行一次隐式转换,"Cherno"是const char数组(C++11后默认),这里需要先转换为std::string,再从string转换为Entity变量,两次隐式转换是不行的,所以会报错。但是写为PrintEntity(std::string(“Cherno”))就可以进行隐式转换。
个人理解:C++编译器认为PrintEntity方法传入的应该是一个const类型的iEntity对象,然而如果它发现传入的参数不是该对象时,就会使用那个类中可以使用传入参数的构造函数来创建一个临时对象。我们例子中传参22是个int型数据,而int_proxy正好有一个携带int参数的构造函数。传入的‘’Cherno‘’是const char型,而类中是shring,所以不行
2.应尽量避免隐式转换
3.explicit 关键字
构造函数前面加上explicit,这意味着这个构造函数不会进行隐式转换
explicit是用来当你想要显示地调用构造函数,而不是让C++编译器隐式地把任何整形转换成Entity
28.C++运算符(操作符)及其重载
1.运算符是给我们使用的一种符号,通常代替一个函数来执行一些事情。比如加减乘除、dereference运算符、箭头运算符、+=运算符、&运算符、左移运算符、new和delete、逗号、圆括号、方括号等等 。
2.运算符定义就是对已有的运算符重新定义,赋予另外一种功能,适应不同的数据类型。
编译器对于两个自定义的数据类型怎么运算?重载
3.应该相当少地使用操作符重载,只在他非常有意义的时候使用。
4.例如JAVA里就没有操作符重载
28.0 无重载
#include <iostream>
struct Vector2
{
float x, y;
Vector2(float x,float y)
:x(x),y(y){}
第一个const是通过引用传递,为了避免复制
第二个const是为了保证不会修改这个结构体
Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}
Vector2 Multiply(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
};
int main()
{
Vector2 position(4.0f, 4.0f);
Vector2 speed(0.5f, 1.5f);
Vector2 powerup(1.1f, 1.1f); //改变speed
Vector2 result1 = position.Add(speed.Multiply(powerup)); //无重载方式 这样写就很麻烦
std::cin.get();
}
28.1 + *运算符重载
operator+ *
1.通过成员函数重载
#include <iostream>
struct Vector2
{
float x, y;
Vector2(float x,float y)
:x(x),y(y){}
Vector2 Add(const Vector2& other) const
{
return Vector2(x + other.x, y + other.y);
}
Vector2 operator+(const Vector2& other) const //定义+操作符
{
return Add(other);
}
Vector2 Multiply(const Vector2& other) const
{
return Vector2(x * other.x, y * other.y);
}
Vector2 operator*(const Vector2& other) const //定义*操作符
{
return Multiply(other);
}
};
int main()
{
Vector2 position(4.0f, 4.0f);
Vector2 speed(0.5f, 1.5f);
Vector2 powerup(1.1f, 1.1f); //改变speed
Vector2 result1 = position.Add(speed); //无重载方式
Vector2 result2 = position.operator+(speed)
简化 Vector2 result2 = position + speed; //重载方式
Vector2 result3 = position.Add(speed.Multiply(powerup)); //无重载方式
Vector2 result3 = position.operator+(speed.operator*(powerup));
简化 Vector2 result3 = position + speed*powerup;
std::cin.get();
}
2.通过全局函数重载
//全局函数实现 + 号运算符重载
Person operator+(const Person& p1, const Person& p2)
Person temp(0, 0);
temp.m_A = p1.m_A + p2.m_A;
temp.m_B = p1.m_B + p2.m_B;
return temp;
}
Person P3 = operator+(P1, P2);
简化 Person P3 = P1 + P2
3.运算符重载也能函数重载
Person operator+(const Person& p2, int val)
{
Person temp;
temp.m_A = p2.m_A + val;
temp.m_B = p2.m_B + val;
return temp;
}
Person p4 = p3 + 10; //相当于 operator+(p3,10)
总结1:对于内置的数据类型的表达式的的运算符是不可能改变的
总结2:不要滥用运算符重载(就是本来想相加,但是在代码里面写了相减)
28.2 左移运算符重载
作用:可以输出自定义数据类型
int a = 10;
cout << a <<endl; //可以输出这个a
Person p;
p.ma = 10;
p.mb = 10;
cout << p <<endl: //这样就不能输出P
1.成员函数重载
//成员函数 实现不了 p << cout 不是我们想要的效果
如果要用成员函数写 成员函数是不是写成这样
Person operator<<(Person& p){}
调用成员函数
p.operator<<(p) 这样明显错了
所以不用成员函数重载左移运算符
2.全局函数重载
根据黑马。一开始不知道什么输出类型就写 void
本质就是 operator<<(cout, p) 简化 cout<<p
cout是标准的输出流对象 并且全局只能有一个,所以要引用& 所以写成 ostream& cout
void operator<<(ostream& out, Person& p) {
out << "a:" << p.m_A << " b:" << p.m_B;
}
但是这样写的话,在调用函数中只能写成
cout << p1
cout << p1 <<endl 这样写就会报错
因为这样可以一直往后是链式编程思想,但是如果重载的函数返回是void 就不能用了。应该要返回cout
这样修改
ostream& operator<<(ostream& out, Person& p) {
out << "a:" << p.m_A << " b:" << p.m_B;
return out;
}
注意,在类中,成员变量默认是私有的,所以的把全局函数定义为友元
class Person {
friend ostream& operator<<(ostream& out, Person& p);
}
总结:重载左移运算符配合友元可以实现输出自定义数据类型
在Cherno代码中,经常写std::,这是因为他开头没写using namespace std;
std::ostream& operator<<(std::ostream& cout, const Vector2& other)
28.3 递增运算符重载
作用: 通过重载递增运算符,实现自己的整型数据
是对自己的数据进行递增,并且输出
所以递增重载包含输出的左移重载和**+重载**
前置
class MyInteger {
friend ostream& operator<<(ostream& out, MyInteger myint);
public:
MyInteger() {
m_Num = 0;
}
//前置++ +重载 这里返回引用为了一直对一个数据进行递增
MyInteger& operator++() {
m_Num++; //先++
return *this; //再自身返回 不然cout不知道输出是谁
}
private:
int m_Num;
};
输出的左移重载
ostream& operator<<(ostream& out, MyInteger myint) {
out << myint.m_Num;
return out;
}
//前置++ 先++ 再返回
void test01() {
MyInteger myInt;
cout << ++myInt << endl;
cout << myInt << endl;
}
一定要注意。前置++返回的是引用为了一直对一个数据进行递增
后置,
将上面成员函数换成下面。但是要注意两点
1.要加上参数int,代表占位参数,可以用于区分前置和后置递增,有int就是后置递增
2.返回的是值,不是引用,因为下面函数运行完后,局部对象temp会释放掉,回引用会出错。
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}
28.4 关系运算符重载(<,>,==,!=)
可以让两个自定义类型对象进行对比操作
黑马的例子
class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
this->m_Age = age;
};
bool operator==(Person & p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return true; 返回true和false,所以函数类似为bool类型
}
else
{
return false;
}
}
bool operator!=(Person & p)
{
if (this->m_Name == p.m_Name && this->m_Age == p.m_Age)
{
return false;
}
else
{
return true;
}
}
string m_Name;
int m_Age;
};
a==b / a.operator==(b)
Cherno的
struct Vector2
{
....
bool operator==(const Vector2& other) const //定义操作符的重载,如果!=,这里做相应修改即可
{
return x == other.x && y == other.y;
}
bool operator!=(const Vector2& other) const //如果!=,这里做相应修改即可
{
return !(*this == other);
}
};
两个都是一个原理
28.5 函数调用运算符重载
函数调用运算符 () 也可以重载
由于重载后使用的方式非常像函数的调用,因此称为仿函数,在STL课程里面用的比较多
仿函数没有固定写法,非常灵活,是指返回类型依赖需求可以多变
#include<string>
using namespace std;
class MyPrint
{
public:
void operator()(string text)
{
cout << text << endl;
}
};
void MyPrint02(string text){
cout << test << endl;
}
void test01()
{
//重载的()操作符 也称为仿函数
MyPrint myFunc;
myFunc("hello world"); 类似函数调用,称为仿函数 myFunc.operator("hello world")
也可以这样写
cout << MyPrint()("hello world") << endl:
MyPrint()为匿名对象,其有个特点,运行完直接被释放,这里重载了(),也可以称为匿名函数对象
MyPrint02("hello world"); 函数调用
}
29. C++对象模型和this指针
29.1 成员变量和成员函数分开存储
1.在C++中,类内的成员变量和成员函数分开存储
2.只有非静态成员变量才属于类的对象上,非静态成员函数不属于
3.函数也不占对象空间,所有函数共享一个函数实例
4.静态成员函数也不占对象空间
5.空对象占用内存空间为:1
c++编译器会给每个对象也分配一个字节空间,是为了区分空对象占内存的位置
29.2 this指针概念
1.黑马的意思:
每一个非静态成员函数只会诞生一份函数实例,所以如果有很多个对象,所以就会指向这一块代码,所以这一块代码如何区分是哪个对象调用的
2.c++通过提供特殊的对象指针,this指针,解决上述问题。this指针指向被调用的成员函数所属的对象
就是如果P1调用这个函数,this指向P1
也就是说 this是指向这个函数所属的当前对象实例的指针
所以我们可以在C++中写一个非静态的方法,为了去调用这个方法,我们需要先实例化一个对象,然后再去调用这个方法,所以这个方法必须由一个有效对象来调用,而this关键字就是指向那个对象的指针
3.this指针是隐含每一个非静态成员函数内的一种指针,是非静态成员函数
this指针不需要定义,直接使用即可
this指针的本质是 指针常量 指针指向是不可以修改的
4.this指针的用途:
4.1当形参和成员变量同名时,可用this指针来区分
4.2*在类的非静态成员函数中返回对象本身,可使用return this
43如果想在类内部调用一个外部的函数,这个函数接受类作为参数,这个时候就可以用this
4.1
class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
age = age; 错
}
int age;
};
不用this指针的话,成员变量和新参就会被认为是一个
4.2
class Person
{
public:
Person(int age)
{
//1、当形参和成员变量同名时,可用this指针来区分
this->age = age;
}
Person& PersonAddPerson(Person p) 这里返回的是引用,也之后还是P2
{ 如果返回值的话,之后就会产生一个新的对象P2‘
this->age += p.age;
//返回对象本身
return *this;
}
int age;
};
void test01()
{
Person p1(10);
cout << "p1.age = " << p1.age << endl;
Person p2(10);
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1);
如果返回引用,后续就一直在P2上操作 结果就是40
返回的是值,第一个调用完结束后,就会产生新的对象P2’,结果就是20.
因为之后就不再P2上调用了,会变成P2‘’ P2‘’‘
cout << "p2.age = " << p2.age << endl;
}
int main() {
test01();
system("pause");
return 0;
}
4.3
class Entity; //前置声明。
void PrintEntity(Entity* e); //在这里声明
class Entity
{
public:
int x,y;
Entity(int x, int y)
{
// Entity* e = this;
this->x = x;
this->y = y;
PrintEntity(this); //我们希望能在这个类里调用PrintEntity,就可以传入this,这就会传入我已经设置了x和y的当前实例
}
};
void PrintEntity(Entity* e) //在这里定义
{
//print something
}
//如果我想传入一个常量引用,我要做的就是在这里进行解引用this
void PrintEntity(const Entity& e); //传入常量引用
class Entity
{
public:
int x,y;
Entity(int x, inty)
{
// Entity* e = this;
this->x = x;
this->y = y;
PrintEntity(*this); // 解引用
}
};
void PrintEntity(const Entity& e)
{
//print something
}
29.3 空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意这个成员函数有没有用到this指针
有的话可能会出错
30. 内存分区模型
C++程序在执行时,将内存大方向划分为4个区域
代码区:存放函数体的二进制代码,由操作系统进行管理的
全局区:存放全局变量和静态变量以及常量
栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区意义:
不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程
30.1 程序运行前
在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域
1.代码区:
存放 CPU 执行的机器指令 自己的代码
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令
2.全局区:
全局变量和静态变量存放在此.
全局区还包含了**常量区, 字符串常量和其他常量(const修饰的变量)**也存放在此.
该区域的数据在程序结束后由操作系统释放.
30.1 程序运行后
1.栈区:
由编译器自动分配释放, 存放函数的参数值,局部变量等
注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
栈区的数据在函数执行完后自动释放
2.堆区:
由程序员分配释放,若程序员不释放,程序结束时由操作系统回收
在C++中主要利用new在堆区开辟内存
int* a = new int(10);
开辟个int形的值为10
注意: 指本质也是局部变量,在栈上,但指针保存的数据是放在堆上
30.2 new操作符
C++中利用new操作符在堆区开辟数据
堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete
语法: new 数据类型
利用new创建的数据,会返回该数据对应的类型的指针
示例1: 基本语法
示例2:开辟数组
int* func()
{ new返回 该数据类型的指针
int* a = new int(10);
return a;
}
int main() {
int *p = func();
cout << *p << endl;
cout << *p << endl;
//利用delete释放堆区数据
delete p;
//cout << *p << endl; //报错,释放的空间不可访问
system("pause");
return 0;
}
//堆区开辟数组
int main() {
int* arr = new int[10]; []意思是数组10个原始 ()是存一个10
for (int i = 0; i < 10; i++){
arr[i] = i + 100;
}
for (int i = 0; i < 10; i++){
cout << arr[i] << endl;
}
//释放数组 delete 后加 []
delete[] arr;
system("pause");
return 0;
}
30.2 C++的对象生存期(栈作用域生存期)
- 基于栈的变量生存周期是什么意思
这些分为两部分:一个是你必须要明白对象是如何生存在栈上的,这样你才会写出能正常工作不会崩溃的代码 - 作用域可以是任何东西,比如说函数作用域,还有像if语句作用域,或者for和while循环作用域,或者空作用域、类作用域。
- 基于栈的变量在我们离开作用域的时候就会被摧毁,内存被释放。在堆上创建的,当程序结束后才会被系统摧毁。
- 局部作用域创建数组的经典错误
例如:返回一个在作用域内创建的数组
如下代码,因为我们没有使用new关键字,所以他不是在堆上分配的,我们只是在栈上分配了这个数组,当我们返回一个指向他的指针时(return array),也就是返回了一个指向栈内存的指针,旦离开这个作用域(CreateArray函数的作用域),这个栈内存就会被回收
int CreateArray()
{
int array[50]; //在栈上创建的
return array;
}
int main()
{
int* a = CreateArray(); //不能正常工作
}
如果你想要像这样写一个函数,那你一般有两个选择
1.在堆上分配这个数组,这样他就会一直存在 new
2.将创建的数组赋值给一个在这个作用域外的变量
智能指针,作用域指针引出
创建Entity对象时,我还是想在堆上分配它,但是我想要在跳出作用域时自动删除它,这样能做到吗?我们可以使用标准库中的作用域指针unique_ptr实现。
#include <iostream>
class Entity
{
private:
public:
Entity()
{
std::cout << "Create!" << std::endl;
}
~Entity()
{
std::cout << "Destroy!" << std::endl;
}
};
class ScopedPtr
{
private:
Entity* m_Ptr;
public:
ScopedPtr(Entity* ptr)
: m_Ptr(ptr)
{
}
~ScopedPtr()
{
delete m_Ptr;
}
};
int main()
{
{
ScopedPtr test = new Entity(); //发生隐式转换。虽然这里是new创建的,但是不同的是一旦超出这个作用域,他就会被销毁。因为这个ScopedPtr类的对象是在栈上分配的
}
std::cin.get();
}
31. C++的智能指针 (感觉自己一知半解)
- 智能指针本质上是原始指针的包装。当你创建一个智能指针,它会调用new并为你分配内存,然后基于你使用的智能指针,这些内存会在某一时刻自动释放。不用调用delete
- .优先使用unique_ptr(作用域指针),其次考虑shared_ptr
尽量使用unique_ptr因为它有一个较低的开销,但如果你需要在对象之间共享,不能使用unique_ptr的时候,就使用shared_ptr
3.要访问所有这些智能指针,你首先要做的是包含memory头文件 - 作用域指针unique_ptr的使用
3.1 unique_ptr是唯一的,不可复制,不可分享。
如果复制一个unique_ptr,会有两个指针,两个unique_ptr指向同一个内存块,如果其中一个死了,它会释放那段内存,也就是说,指向同一块内存的第二个unique_ptr指向了已经被释放的内存。
3.2 unique_ptr是作用域指针,意味着超出作用域时,调用delete销毁他
3.3 unique_ptr构造函数实际上是explicit的,没有构造函数的隐式转换,需要显式调用构造函数。
unique_ptr<类名> 作用域指针名字(new 类名())
unique_ptr entity(new Entity());
unique_ptr entity = new Entity();不行,需要显示调用
3.4 最好使用std::unique_ptr<Entity> entity = std::make_unique<Entity>()
因为如果构造函数碰巧抛出异常,不会得到一个没有引用的悬空指针从而造成内存泄露,它会稍微安全一些。
std::make_unique<>()是在C++14引入的,C++11不支持 - 共享指针shared_ptr 的使用
4.1 shared_ptr的工作方式是通过引用计数。
引用计数基本上是一种方法,可以跟踪你的指针有多少个引用,一旦引用计数达到零,他就被删除了。
例如:我创建了一个共享指针shared_ptr,我又创建了另一个shared_ptr来复制它,我的引用计数是2,第一个和第二个,共2个。当第一个死的时候,我的引用计数器现在减少1,然后当最后一个shared_ptr死了,我的引用计数回到零,内存就被释放。
4.3 shared_ptr需要分配另一块内存,叫做控制块,用来存储引用计数,如果您首先创建一个new Entity,然后将其传递给shared_ptr构造函数,它必须分配,做2次内存分配。先做一次new Entity的分配,然后是shared_ptr的控制内存块的分配。然而如果你用make_shared你能把它们组合起来,这样更有效率
std::shared_ptr sharedEntity = sharedEntity(new Entity());//不推荐!
std::shared_ptr sharedEntity = std::make_shared();//ok
5.弱指针weak_ptr
5.1 可以和共享指针shared_ptr一起使用
5.2.weak_ptr可以被复制,但是同时不会增加额外的控制块来控制计数,仅仅声明这个指针还活着。
当你将一个shared_ptr赋值给另外一个shared_ptr,引用计数++,而若是把一个shared_ptr赋值给一个weak_ptr时,它不会增加引用计数。这很好,如果你不想要Entity的所有权,就像你可能在排序一个Entity列表,你不关心它们是否有效,你只需要存储它们的一个引用就可以了。
32. 函数默认参数
在C++中,函数的形参列表中的形参是可以有默认值的。
语法: 返回值类型 函数名 (参数= 默认值){}
- 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
- 如果函数声明有默认值,函数实现的时候就不能有默认参数
声明和实现只能有一个默认参数
33. 函数占位参数
C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
语法:== 返回值类型 函数名 (数据类型){}==
34. 函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
同一个作用域下
函数名称相同
函数参数类型不同 或者 参数个数不同 或者 参数顺序不同
//函数重载需要函数都在同一个作用域下
void func()
{
cout << "func 的调用!" << endl;
}
void func(int a)
{
cout << "func (int a) 的调用!" << endl;
}
void func(double a)
{
cout << "func (double a)的调用!" << endl;
}
void func(int a ,double b)
{
cout << "func (int a ,double b) 的调用!" << endl;
}
void func(double a ,int b)
{
cout << "func (double a ,int b)的调用!" << endl;
}
注意: 函数的返回值不可以作为函数重载的条件
也就是返回值为void 不能为函数类型(如int)
2.注意事项
2.1 引用作为重载条件
//1、引用作为重载条件
void func(int &a){
cout << "func (int &a) 调用 " << endl;
}
void func(const int &a){
cout << "func (const int &a) 调用 " << endl;
}
int main() {
int a = 10;
func(a); 调用无const 因为a是变量 可读可写 const修饰的话就是代表变量只读
func(10);调用有const 无const的话 int& a=10不合法
引用必须在合法的内存空间 要么在栈区 要么在堆区
10 是全局区中的常量区
2.2 函数重载碰到函数默认参数
碰到默认参数产生二义性,需要避免
//2、函数重载碰到函数默认参数
void func2(int a, int b = 10){
cout << "func2(int a, int b = 10) 调用" << endl;
}
void func2(int a){
cout << "func2(int a) 调用" << endl;
}
int main() {
int a = 10;
func2(10); 这里就出现二义性,不知道调哪一个
system("pause");
return 0;
}
35. 类和对象(黑马视频回顾一下)
C++面向对象的三大特性为:封装、继承、多态
C++认为万事万物都皆为对象,对象上有其属性和行为
例如:
人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…
车也可以作为对象,属性有轮胎、方向盘、车灯…,行为有载人、放音乐、放空调…
具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类
35.1 封装
35.1.1 封装的意义
封装是C++面向对象三大特性之一
封装的意义:
将属性和行为作为一个整体,表现生活中的事物。
将属性和行为加以权限控制
语法:class 类名{ 访问权限: 属性 / 行为 };
示例:设计一个圆类,求圆的周长
//圆周率
const double PI = 3.14;
1、封装的意义
意义一:将属性和行为作为一个整体,用来表现生活中的事物
封装一个圆类,求圆的周长
class代表设计一个类,后面跟着的是类名
class Circle
{
public: //访问权限 公共的权限
//属性
int m_r;//半径
//行为
//获取到圆的周长
double calculateZC()
{
//2 * pi * r
//获取圆的周长
return 2 * PI * m_r;
}
};
int main() {
//通过圆类,创建圆的对象
// c1就是一个具体的圆
Circle c1;
c1.m_r = 10; //给圆对象的半径 进行赋值操作
//2 * pi * 10 = = 62.8
cout << "圆的周长为: " << c1.calculateZC() << endl;
system("pause");
return 0;
}
意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制
可以见17
访问权限有三种:
public 公共权限
protected 保护权限
private 私有权限
private:只有自己的类和它的**友元(friend)**才能访问(继承的子类也不行,友元的意思就是可以允许你访问这个类的私有成员)。
protected:这个类以及它的所有派生类都可以访问到这些成员。(但在main函数中new一个类就不可见,这其实是因为main函数不是类的函数,对main函数是不可访问的)
public:谁都可见。
35.1.2 struct和class区别
在C++中 struct和class唯一的区别就在于 默认的访问权限不同
- 区别:class默认private,struct默认public
- struct是为了让C++向后兼容C。 #define struct class
- 如何选这两个
1.若只包含一些变量结构或POD(plain old data)时,选用struct。例如数学中的向量类。
2.struct一般不用继承,继承是一种增加另一层次复杂性的东西,结构体是数据的结构
3.若要实现很多功能的类,则选用class
35.2 对象的初始化和清理
1.生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全
2.C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置。
35.2.1 构造函数和析构函数
-
对象的初始化和清理也是两个非常重要的安全问题
一个对象或者变量没有初始状态,对其使用后果是未知
同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题
c++利用了构造函数和析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。 -
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供默认、
-
构造函数语法:类名(){}
1.构造函数,没有返回值也不写void
2.函数名称与类名相同
3.构造函数可以有参数,因此可以发生重载
4.程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
5.如果你不指定构造函数,你仍然有一个构造函数,这叫做默认构造函数,但仍然可以删除该默认构造函数 -
析构函数语法:~类名(){}
1.析构函数,没有返回值也不写void
2.函数名称与类名相同,在名称前加上符号 ~
3.析构函数不可以有参数,因此不可以发生重载
4.程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次、
5.析构函数同时适用于栈和堆分配的内存。
1)因此如果你用new关键字创建一个对象(存在于堆上),然后你调用delete,析构函数就会被调用。
2)如果你只有基于栈的对象,当跳出作用域的时候这个对象会被删除,所以这时侯析构函数也会被调用
35.2.2 构造函数的分类及调用
1.两种分类方式:
按参数分为: 有参构造和无参构造
按类型分为: 普通构造和拷贝构造
class Person {
public:
无参(默认)构造函数
Person() {
cout << "无参构造函数!" << endl;
}
有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
拷贝构造函数 就是复制另外一个类,并且不能修改,所以是const &
Person(const Person& p) {
age = p.age;
cout << "拷贝构造函数!" << endl;
}
}
2.三种调用方式:
括号法
显示法
隐式转换法
2、构造函数的调用
void test02() {
2.1 括号法,常用
Person p; 调用无参构造函数
Person p1(10); 调用有参构造函数
Person p2(p1); 调用拷贝构造函数
注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
Person p2();
2.2 显式法
Person p2 = Person(10); 调用有参构造函数
Person p3 = Person(p2); 调用拷贝构造函数
注:1.Person(10)单独写就是匿名对象 当前 行 结束之后,系统会马上回收匿名对象
2.不能利用 拷贝构造函数 初始化匿名对象 Person (p4);
编译器认为是对象声明 即 Person (p4)==Person p4
2.3 隐式转换法
Person p4 = 10; // Person p4 = Person(10);
Person p5 = p4; // Person p5 = Person(p4);
}
35.2.3 拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
Person man(100); p对象已经创建完毕
Person newman(man); 调用拷贝构造函数 括号法
Person newman2 = man; 调用拷贝构造 隐式转换法
等于 Person newman2 = Person(man)
Person newman3;
newman3 = man; 不是调用拷贝构造函数,这是赋值操作
}
2. 值传递的方式给函数参数传值
相当于Person p1 = p; 隐式转换法
void doWork(Person p1) {
实参传给形参,会调用拷贝工造函数
注意:修改P1的值不会改变P的值,因为值传递是拷贝一个临时的副本
}
void test02() {
Person p; //无参构造函数
doWork(p);
}
3. 以值方式返回局部对象
Person doWork2()
{
Person p1;
cout << (int *)&p1 << endl;
return p1; 因为是值返回,所以返回的不是P1,是根据P1创建的新的对象
}
void test03()
{
Person p = doWork2();
cout << (int *)&p << endl;
}
35.2.4 构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
如用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
35.2.5 深拷贝与浅拷贝
深浅拷贝是面试经典问题,也是常见的一个坑
浅拷贝:简单的赋值拷贝操作,编译器提供的,
深拷贝:在堆区重新申请空间,进行拷贝操作, 自己写一个
深拷贝个人理解:就是多个堆区指向的类容一样,就是地址不一样
1.浅拷贝 简单的赋值 Person p2(p1);
2.深拷贝
在类中用new开辟数据,必须用深拷贝
记住一句话 栈区先进后出
class Person {
public:
//有参构造函数
Person(int age ,int height) {
cout << "有参构造函数!" << endl;
m_age = age;
m_height = new int(height);
}
//拷贝构造函数
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
m_age = p.m_age;
m_height = new int(*p.m_height);
}
//析构函数 堆区有内存,所以在调用析构就得delete删除内存
~Person() {
cout << "析构函数!" << endl;
if (m_height != NULL)
{
delete m_height;
}
}
public:
int m_age;
int* m_height;
};
void test01()
{
Person p1(18, 180);
Person p2(p1);
如果不进行深拷贝,那按照栈先进后出,则P2先释放,那int* m_height指向的数据会释放掉,
所以P1 的m_height指向数据也就没有了,所以会出错
cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}
35.2.6 初始化列表
C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)… {}
35.2.7 类对象作为类成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
例如:
class A {}
class B
{
A a;
}
B类中有对象A作为成员,A为对象成员
那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?
构造的顺序是 :先调用对象成员的构造,再调用本类构造
析构顺序与构造相反
就是:
A构造
B构造
B析构
A析构
35.2.8 静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
- 静态成员变量
1.1 所有对象共享同一份数据
1.2 在编译阶段分配内存 全局区
1.3 类内声明,类外初始化 - 静态成员函数
2.1 所有对象共享同一个函数
2.2 静态成员函数只能访问静态成员变量
1.类外初始化
类型 类名::变量名 = 值;
2.静态成员变量 不属于某个对象 所有对象共享同一份数据
3.静态成员变量两种访问方式
1、通过对象
Person p1;
p1.m_A = 100;
cout << "p1.m_A = " << p1.m_A << endl
2、通过类名
cout << "m_A = " << Person::m_A << endl;
4.静态成员变量也是有访问权限的,类外私有权限访问不到
/静态成员函数特点:
1.程序共享一个函数
2.静态成员函数只能访问静态成员变量 不可以访问非静态成员变量
3.静态成员函数也是有访问权限的 私有权限访问不到
4.静态成员变量两种访问方式
1、通过对象
Person p1;
p1.func();
2、通过类名 静态成员函数不属于某个对象 所以可以通过类名访问
Person::func();
35.3 友元
在程序里,有些私有属性 也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的就是让一个函数或者类 访问另一个类中私有成员
友元的关键字为 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
35.3.1 全局函数做友元
告诉编译器 全局函数 是 类的好朋友,可以访问类中的私有内容
class 类名{
friend 全局函数(); 写在类的最上面
}
35.3.2 类做友元
某个类可以访问其他类的私有
告诉编译器 类a 是 类b的好朋友,可以访问类中的私有内容
class 类b{
friend class 类a; 写在类的最上面
}
35.3.3 成员函数做友元
某个类的成员函数访问其他类的私有
告诉编译器 类a 是 类b的好朋友,可以访问类中的私有内容
告诉编译器 类a中的成员函数 是 类b的好朋友,可以访问类中的私有内容
class 类b{
friend 类型(如void) 类a::成员函数(); 写在类的最上面
}
35.4 继承
继承是面向对象三大特性之一
我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码
35.4.1 继承的基本语法
继承的好处:可以减少重复的代码
class A : public B;
A 类称为子类 或 派生类
B 类称为父类 或 基类
派生类中的成员,包含两大部分:
一类是从基类继承过来的,一类是自己增加的成员。
从基类继承过过来的表现其共性,而新增的成员体现了其个性。
35.4.2 继承的方式
继承方式一共有三种:
公共继承
保护继承
私有继承
三种继承方式,都不能访问基类的private成员。
注意:父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
35.4.3 继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
问题:父类和子类的构造和析构顺序是谁先谁后?
答:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
35.4.4 继承同名成员处理方式 (跟虚函数不同,虚函数是重写子类)
问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?
1.访问子类同名成员 直接访问即可
子类实例.成员/函数
2.访问父类同名成员 需要加作用域
当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
子类实例.父类作用域(父类的类名)::成员/函数
35.4.5 继承同名静态成员处理方式
问题:继承中同名的静态成员在子类对象上如何进行访问?
跟上面一样,但静态有两种访问方式
1.访问子类同名成员 直接访问即可
子类实例.成员/函数
子类名::成员/函数
2.访问父类同名成员 需要加作用域
子类实例.父类作用域(父类的类名)::成员/函数
子类名::父类作用域(父类的类名)::成员/函数
35.4.6 多继承语法
C++允许一个类继承多个类
语法: class 子类 :继承方式 父类1 , 继承方式 父类2…
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
35.4.7 菱形继承
菱形继承概念:
两个派生类继承同一个基类
又有某个类同时继承者两个派生类
这种继承被称为菱形继承,或者钻石继承
菱形继承问题:
羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
利用虚继承可以解决菱形继承问题
class Animal
{
public:
int m_Age;
};
继承前加virtual关键字后,变为虚继承
此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
void test01(){
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}
int main() {
test01();
system("pause");
return 0;
}
35.5 多态
35.5.1 多态的基本概念
多态是C++面向对象三大特性之一
多态分为两类
静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址
下面通过案例进行讲解多态
class Animal
{
public:
Speak函数就是虚函数
函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了,地址就无法早绑定了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
我们希望传入什么对象,那么就调用什么对象的函数
如果函数地址在编译阶段就能确定,那么静态联编
如果函数地址在运行阶段才能确定,就是动态联编
void DoSpeak(Animal & animal)
{
animal.speak();
}
多态满足条件:
1、有继承关系 2、子类重写父类中的虚函数
多态使用:
==父类指针或引用== 指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
void test01()
{
Cat cat;
DoSpeak(cat);
}
不写虚函数,调用的就是Animal,
如果想让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段绑定
多态满足条件:
1、有继承关系 2、子类重写父类中的虚函数
多态使用:
父类指针或引用 指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
原理剖析
多态的优点:
代码组织结构清晰
可读性强
利于前期和后期的扩展以及维护
35.5.2 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
1.无法实例化对象
2.子类必须重写抽象类中的纯虚函数,否则也属于抽象类
35.5.3 虚析构和纯虚析构(为了删除在子类的堆区数据)
-
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
-
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
-
虚析构和纯虚析构共性:
可以解决父类指针释放子类对象
都需要有具体的函数实现 -
虚析构和纯虚析构区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象 -
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0
类名::~类名(){} 类外实现
为什么要用到这个?
6. 通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
怎么解决?给基类增加一个虚析构函数
虚析构函数就是用来解决通过父类指针释放子类对象
7. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构在这里插入代码片
class Animal {
public:
Animal()
{
cout << "Animal 构造函数调用!" << endl;
}
virtual void Speak() = 0;
//析构函数加上virtual关键字,变成虚析构函数
//virtual ~Animal()
//{
// cout << "Animal虚析构函数调用!" << endl;
//}
virtual ~Animal() = 0;
};
Animal::~Animal()
{
cout << "Animal 纯虚析构函数调用!" << endl;
}
和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。
class Cat : public Animal {
public:
Cat(string name)
{
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name);
}
virtual void Speak()
{
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat()
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL) {
delete m_Name;
m_Name = NULL;
}
}
public:
string *m_Name;
};
void test01()
{
Animal *animal = new Cat("Tom");
animal->Speak();
通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
怎么解决?给基类增加一个虚析构函数
虚析构函数就是用来解决通过父类指针释放子类对象
delete animal;
}
int main() {
test01();
system("pause");
return 0;
}