C++ 核心编程
堆空间开辟地址 ——— new
开辟空间
我们利用new
可以在堆区开辟空间,堆区由程序员进行管控,可以手动进行管理数据存放内存的释放与否,若不写释放过程,则堆区数据会在程序结束时由OS
自动释放
开辟空间形式:
1 |
|
如果我们想要访问该堆空间的数据我们可以创建一个对应数据类型的一个指针
1 |
|
如果我们想要在堆上创建一个数组或者字符串呢?
1 |
|
同样的我们需要创建一个指针变量对其进行保存
1 |
|
释放空间
我们学会了如何开辟空间后,那如何进行删除对应堆空间的数据呢?这个时候我们将会用到delete
对于不为字符串指针的我们可以直接按下面方式进行删除:
1 |
|
加入是一个字符串我们需要额外加上一个中括号:
1 |
|
引用 ———— 给变量起别名
本质:给变量取别名
语法:数据类型(与原数据类型相同) &别名 = 原名
相当于将
别名
的地址取成了原名
的地址,进而通过改变对应的内存中的数据
下面给出一个例子:
1 |
|
相关注意事项
-
引用必须要初始化
需要告诉其为什么东西的别名,将其进行初始化
1
int &b; // 错误的
-
引用一旦初始化后就不可以更改了
假设我们初始化 b 为 a 的别名后,不可以再将其进行改变为 c 的别名
1
2
3
4
5int a = 10;
int &b = a;
int c = 20;
b = c; // 更改 a 的值,而不是更改引用
&b = c; // 多次引用会报错
引用做函数参数
作用:函数传参的时候,可以利用引用让形参修饰实参
优点:可以简化指针修改实参
假设我们需要完成一个交换函数,我们除了利用值传递
、地址传递
、我们还可以使用引用传递
值传递:形参不会修饰实参
1 |
|
地址传递:形参会修饰实参
1 |
|
引用传递:形参会修饰实参
1 |
|
引用做函数返回值类型
注意:
- 不要返回局部变量的引用
1 |
|
- 函数的调用可以作为左值(左值: 能用于赋值运算左边的表达式)
1 |
|
引用本质
其在C++
内部实现是一个指针常量( 算是一种懒人版指针? )
1 |
|
常量引用
作用:用来修饰形参,来防止误操作
1 |
|
函数提高
函数默认参数
C++
中,函数的形参列表中的形参是可以有默认值的
语法:返回值类型 函数名(形参 = 默认参数)
正常情况下我们计算一个三个数相加时,需要将三个参数均传入
1 |
|
但是如果我们设定了默认值我们可以只传入一部分参数,其他数据使用默认值
1 |
|
注意事项:
-
当一个形参设置的默认参数时,其后面的形参都需要设置对应的默认参数
-
如果函数的声明有默认参数,函数实现就不能有默认参数
1
2
3
4int func(int a = 10,int b = 10);
int func(int a = 20 ,int b = 20){ // 默认参数重定义了,会发生报错
return a + b;
}
函数占位参数
语法:返回值类型 函数名 (数据类型){}
1 |
|
其中占位参数可以拥有默认参数
1 |
|
函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名相同
- 函数参数类型不同或者个数不同或者顺序不同
注意:函数的返回值不可以作为函数重载的条件
1 |
|
注意事项:
-
引用作为重载条件
1
2
3
4
5
6
7
8
9
10
11
12void func(int& a){
//XXX
}
void func(const int& a){ // const 限制了只读的特点
//XXX
}
int main(){
int a = 10;
func(a); // 调用第一个func()
func(10); // 调用第二个func(),编译器会产生一个中间变量保存10,之后调用函数
} -
函数重载遇到默认参数
1
2
3
4
5
6
7
8
9
10void func(int a){
//XXX
}
void func(int a,int b = 10){
//XXX
}
int main(){
func(10); // 发生错误,两个函数均可以调用,产生了二义性
}
类和对象
C++
面向对象的三大特性为:封装、继承、多态
封装
意义:
-
将属性和行为作为一个整体,表现生活中的事物
-
将属性和行为加以权限控制
语法:
1 |
|
如定义一个圆类:
1 |
|
class
与struct
的唯一的区别就在于默认访问权限,class
在不注明对应的权限时,默认为私有,struct
默认权限为公共
对象的初始化与清理
C++
利用构造函数
和析构函数
解决对象的初始化和清理,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作,如果我们不提供构造和析构函数,编译器会提供,但是其提供的构造和析构函数是空实现。
- 构造函数:主要用于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用
- 析构函数:主要用于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法: 类名(){}
- 构造函数,没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象时候会自动调用构造函数,无须手动调用,而且只会调用一次
析构函数语法:~类名(){}
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号
~
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
构造函数的分类及调用
两种分类方式
按参数分为: 有参构造和无参构造
按类型分为: 普通构造和拷贝构造
三种调用方式:括号法、显示法、隐式转换法
1 |
|
深拷贝与浅拷贝
浅拷贝 -> 简单的赋值拷贝操作
深拷贝 -> 在堆区重新开辟空间,进行拷贝操作
浅拷贝带来的问题是堆区内存的重复释放,对此问题我们采用深拷贝来进行解决
如果属性有在堆区开辟的,一定要自己提供一个构造函数,防止浅拷贝带来的问题
举个例子:
1 |
|
上面例子中因为b
是一个指针,指向一个地址空间,如果我们简单的将对应的堆上地址进行复制过去时便会产生在析构函数中多次进行释放的一个问题,从而导致问题
初始化列表
语法: 构造函数(): 属性1(值1)、属性2(值2)...
我们传统的初始化过程像下面代码:
1 |
|
利用C++
的一个特性我们可以将其进行修改为:
1 |
|
类对象作为类成员
C++
类中的成员可以是另一个类的对象,我们称该成员为对象成员
如:
1 |
|
可以简单的理解为结构体的嵌套,一个类中包括另一个类也是相似的
需要注意的是当其他类的对象作为类成员时,会先调用类成员,再构造自身,以上面的例子来说,先会执行A
的构造函数,随后执行B
的构造函数。而执行析构时则先执行B
,随后再执行A
静态成员
静态成员就算在成员变量和成员函数前加上static
静态成员变量:
-
所有对象共享同一份数据
当存在有另一个类名相同的类对类中一个静态成员进行修改时,那么原先类中的值将会发生改变
1
2
3
4
5
6
7
8
9
10
11
12
13class P{
static int a;
}
int P::a = 100;
void test(){
P p0,p1;
p1.a = 200;
cout<<p0.a; // 200
}
int main(){
test();
};于此同时我们可以通过对象或者类名来进行访问对应的内存(静态成员变量)
1
2
3
4
5// 对象
P p;
cout << p.a << endl;
// 类名
cout << P::a << endl; -
在编译阶段分配内存
-
类内声明,类外初始化
假如我们定义了一个类
1
2
3class P{
static int a; //类内声明
};那么我们还需要在类外进行初始化
1
int P::a = 100;
那么结合起来整个定义过程为:
1
2
3
4class P{
static int a; //类内声明
};
int P::a = 100;
静态成员函数:
-
所有对象共享同一个函数
-
静态成员函数只能访问静态成员变量
举个例子:
1
2
3
4
5
6
7
8
9class P{
static void func(){
a = 100; // 可以进行操作
b = 100; // 非法操作
}
static int a = 100;
int b;
};
int P::a = 0;a
作为静态成员时可以被其访问到,而b
作为一个普通变量无法被访问到
C++ 对象模型和this指针
成员变量和成员函数分开存储
C++
中,类内成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上
空对象占用内存空间为 1
,因为C++
编译器会给每一个空对象也分配一个字节空间,是为了区分空对象占内存的位置
不为空时,类的大小就算类内成员所占空间的大小
需要注意的是,仅有非静态成员变量属于类的对象上,像静态成员变量、非静态成员函数、静态成员函数都不属于类对象上
1
2
3
4
5
6
class P{
int a;
static int b;
void func1(){}
static func2(){}
};此时
P
类的大小还是只计算一个int a
的大小,即为 4
this指针
C++
中成员变量和成员函数是分开存储的,每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
C++
通过this
指针指向被调用的成员函数所属的对象
this
指针式隐含每一个非静态成员函数内的一种指针,this
指针不需要定义,直接使用即可
this
指针用途:
-
当形参与成员变量同名时,可以用
this
指针来区分举个例子:
1
2
3
4
5
6
7
8
9
10
11class Person{
public:
Person(int age){
age = age;
}
int age;
};
int main(){
Person p1(18)
cout<<p1.age;
}我们可以发现程序的输出并不是
18
,而是一个随机的值,为了解决中国问题我们可以使用this
指针来指向被调用的成员函数所属的对象1
2
3
4
5
6
7
8
9
10
11class Person{
public:
Person(int age){
this -> age = age;
}
int age;
};
int main(){
Person p1(18)
cout<<p1.age;
}也就是
this
指向本类中的age
变量来避免了重名的问题 -
在类的非静态成员函数中返回对象本身,可使用
return *this
举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Person{
public:
Person(int age){
this -> age = age;
}
void PersonAgeAdd(Person &p){
this -> age += p.age;
}
int age;
};
int main(){
Person p1(18)
Person p2(10)
p1.PersonAgeAdd(p2)
cout<<p1.age;
}上面代码是我们一般实现一个年纪加法的一种运算,对此我们如果想要再次实现一个加法需要再加一行,再加上一行的
p1.PersonAgeAdd(p2)
但是
C++
提供的this
指针,可以给我们创建点便利,像Python
代码一样使用多个.
来进行链接,达到p1.PersonAgeAdd(p2).PersonAgeAdd(p2)
的效果1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class Person{
public:
Person(int age){
this -> age = age;
}
Person& PersonAgeAdd(Person &p){ // 需要使用引用的方式来进行返回
this -> age += p.age;
return *this; // 返回的是Person本身这个实体
}
int age;
};
int main(){
Person p1(18)
Person p2(10)
p1.PersonAgeAdd(p2).PersonAgeAdd(p2)
cout<<p1.age;
}需要注意的是,我们如果将其不小心写成了一个值的返回
1
2
3
4Person PersonAgeAdd(Person &p){ // 引用的方式来进行返回被我们更改为一个值的类型
this -> age += p.age;
return *this; // 返回的是不再是Person本身这个实体,而是创建了一个新的Person实体,可以理解为Person’
}我们如果是上面的写法的话,那我们得到的永远是Person的一个新实例,而不是原先的对象本身,导致我们想获取到的数据发生问题
空指针访问成员函数
C++
空指针也是可以调用成员函数的,但是也要注意到有没有用到this
指针
如果用到了this
指针,需要加以判断保证代码健壮性
空指针可以访问对应的对象,但是不能访问对象中的变量
1 |
|
上面例子中,因为我们没有对this
指针进行判断,当我们新创建的一个P
指针指向对应age
变量时,引发了读取访问权限异常
对此我们可以加入一个判断,来确保this
指针非空
1 |
|
const修饰成员函数
常函数
-
成员函数后加
const
后,我们称这个函数为常函数成员函数后面加
const
,修改的是this
指向,让指针指向的值也不可以修改 -
常函数内不可以修改成员属性
-
成员属性声明时加关键字
mutable
后,常函数中依然可以修改假如我们有一个类
1
2
3
4
5
6class Person{
void test() const {
this->age = 20;
}
int age;
};可以看到
this->age = 20;
处会发生问题,显示无法修改,假如我们要实现修改什么班呢,我们可以加上mutable1
2
3
4
5
6class Person{
void test() const {
this->age = 20;
}
mutable int age;
};此时我们便可以进行对应的修改
常对象
- 声明对象前加
const
称该对象为常对象 - 常对象只能调用常函数
友元
友元的目的就是让一个函数或者类,访问另一个类中的私有函数
关键字:friend
友元的实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
全局函数做友元
我们创建一个全局函数,之后将其在类中进行声明加上friend
即可
1 |
|
类做友元
与全局函数做友元一样,我们同样创建一个类之后,在目的类中加上一个friend
声明即可
1 |
|
成员函数做友元
和前面的方式一样,我们在对应的类中加上对应的作用域以及friend
即可
1 |
|
运算符重载
对已有的运算符重新进行定义,赋予另一种功能以适应不同的需要
关键字:operator
我们可以通过operator
来完成一种类型的变量和另一种类型的变量相互运算
加法运算重载
1 |
|
左移运算重载
1 |
|
递增运算符重载
重载前置++运算符
1 |
|
前置++
时,++
在前,先完成的是自增运算,随后进行赋值,因此我们输出的结果是自增后的值
重载后置++运算符
1 |
|
后置++
时,++
在后,先完成的是输出值,随后进行自增,因此我们输出的结果是自增前的值
赋值运算符重载
C++
编译器至少给一个类添加 4 个函数
- 默认构造函数
- 默认析构函数
- 默认拷贝函数,对属性进行值拷贝
- 赋值运算符
operator=
对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
1 |
|
关系运算符重载
1 |
|
函数调用运算符重载
-
函数调用
()
也可以重载 -
用于重载后使用的方式非常像函数的的调用,因此称为仿函数
-
仿函数没有固定写法,非常灵活
简单的加法实现
1 |
|
使用匿名函数对象实现
1 |
|
继承
基本语法
语法:class 子类 : 继承方式 父类
子类也称为派生类,父类也称为基类
1 |
|
继承可以减少重复的代码,派生类中的成员包含两大部分:
-
一类是从积累继承过来的,一类是自己增加的成员
-
从基类继承过来的表现其共性,而新增的成员体现了其个性
继承方式
继承方式有三种:公共继承、保护继承、私有继承
继承中的对象模型
父类中的私有成员只是被隐藏了,但是还是会继承下去
我们可以通过VS
的开发人员命令提示符,来大型我们想要查看的类的结构
1 |
|
继承中构造和析构顺序
子类继承父类后,当创建子类对象,也会调用父类的构造函数
那么便会产生一个问题,父类与子类的构造函数和析构函数是谁先谁后
1 |
|
我们构造上面的例子我们调用Son
类型时,其输出的值顺序为1342
,说明继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
继承中同名成员处理方式
当父类与子类中出现同名的成员我们进行访问对应成员时需要注意:
访问子类同名成员直接访问即可,当访问父类同名成员时需要加上作用域
1 |
|
如果子类中出现了和父类同名的成员函数,子类的同名成员会隐藏掉父类中所所有同名成员函数
继承中同名静态成员处理方式
与同名非静态成员的处理方式一样,访问子类同名成员直接访问即可,当访问父类同名成员时需要加上作用域
我们可以通过对象方式进行访问或者我们可以通过类名加上对应作用域来进行访问
1 |
|
对于成员同名静态成员函数,也会隐藏父类中所有同名成员函数,如果想访问父类中静态成员函数,需要同样加上对应作用域
多继承语法
语法:class 子类:继承方式 父类1 , 继承方式 父类2
1 |
|
菱形继承
概念:两个派生类继承同一个基类,又有某个类同时继承两个派生类,这种继承被称为菱形继承
当我们继承了两个类时,我们可以意识到这两个类都继承了基类,那么基类便会有重复,因此我们需要解决这个问题,我们可以采用虚继承来解决这个问题
1 |
|
多态
多态优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
两者区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
如:
1 |
|
上述代码中可以显示出的是saying
,这是因为执行说话的函数地址早绑定,在编译阶段就可以确定函数的地址,加入我们想要让输出的是cat is saying
那么这个函数的地址就不能提前绑定,需要在运行阶段中进行晚绑定
1 |
|
动态多态需要满足有继承关系,子类要重写父类的虚函数
动态多态使用,父类的指针或者引用指向子类对象
多态原理分析
当我们使用virtual
关键字时,其会创建一个虚函数(表)指针 ———— vfptr
占4字节
我们子类继承使用virtual
时便会将父类中的虚函数表指针进行替换,替换成子类自己的函数指针,因此当父类的指针或者引用指向子类对象的时候发生多态
纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此我们可以将虚函数改为纯虚函数,对应语法:virtual 返回值类型 函数名 (参数列表) = 0
,当类中有了存虚函数,这个类也称为抽象类
抽象类特点
:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
如:
1 |
|
可以理解为我们父类提供一个指向,需要通过子类来进行实现具体的过程,因此我们在每次扩展功能时不需要去修改原来的代码,仅需要往后扩展即可。
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯系析构区别:如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名(){}
虚析构语法:virtual ~类名() = 0; 类名::~类名(){}
虚析构和纯虚构函数都必须有相应实现
父类指针在析构的时候不会调用子类的析构函数,因此父类指针释放子类对象时会存在不干净的问题。
虚析构和纯虚析构就是用来解决通过父类指针释放子类对象
如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
用于纯虚析构函数的类也属于抽象类
文件操作
C++
中的文件操作需要包含头文件#include <fstream>
操作文件的三大类:
ofstream
:写操作ifstream
:读操作fstream
:读写操作
创建流对象:ofstream ofs
打开文件:ofs.open("<file path>",<mode>)
打开方式 解释 ios::in 读文件 ios::out 写文件 ios::ate 初始位置:文件尾 ios::app 追加方式写文件 ios::trunc 如果文件存在先删除,再创建 ios::binary 二进制方式 注意:文件打开方式可以配合使用,使用
|
操作符
写数据:ofs << "XXX";
关闭文件:ofs.close()
读文件
使用>>
1 |
|
使用getline()
1 |
|
或者
1 |
|
使用get()
1 |
|