CPP语言
new和delete
C++是一门面向对象的高级语言,在我们编写代码中,常常离不开对对象的创建和清理对象资源。而兼容过来的malloc和free并不能很好的满足我们的需求,从而C++将malloc和free封装起来并起了新的名字new和delete,这两个关键字的作用不仅比malloc和free的功能强大,用起来也非常的方便,下面我们来看看new和delete的用法
new 和 delete对内置类型的操作
new和delete都是运算符,不是库函数,不需要单独添加头文件
格式:
new1、类型指针 指针变量名 = new 类型
2、类型指针 指针变量名 = new 类型(初始值)
3、类型指针 指针变量名 = new 类型[元素个数]
delete1、delete 指针变量名
2、delete[] 指针变量名
示例:
int *p = new int // 随机值
delete p; // 释放p指向内存空间
int *p = new int(0); // 指定值
delete p;
int *p = new int(); // 零值
delete p;
int *p = new int[3]; //三个随机值
delete[] p;
int *p = new int[3]{0,0,0}; //三个0
delete[] p;
int* func()
{
int* a = new int(10);
return a;
}
int main() {
int *p = func();
cout << *p << endl;
cout << *p << endl;
//利用delete释放堆区数据
delete p;
//cout << *p << endl; //报错,释放的空间不可访问
return 0;
}
//堆区开辟数组
int main() {
int* arr = new int[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;
return 0;
}
new & malloc 区别
总结
new
是C++的操作符,而malloc
是C语言的函数。new
可以直接调用类的构造函数来初始化对象,而malloc
只是简单地分配内存空间,不会调用构造函数。new
操作符会根据所需类型的大小自动计算内存大小,而malloc
函数需要显式指定内存大小。new
操作符返回所分配类型的指针,而malloc
函数返回void*
类型的指针。new
操作符在内存分配失败时会抛出异常,而malloc
函数在内存分配失败时返回NULL
指针。new
操作符可以被重载,而malloc
函数不能被重载,因为malloc是C语言的库函数,C语言没有重载。
返回值 名字 参数 体 错误
new 不需要强转,malloc 需要强转
new是运算符,malloc是库函数
new 不需要传入具体的字节
new 会先调用malloc再调用构造函数
new会抛出异常,malloc返回空
栈的大小
Win 2M Linux 8M
函数内申请大数组,递归层数过深会导致爆栈
杂项
语法
- 主函数执行之前会执行静态成员变量的初始化和全局变量的初始化
- INT_MAX INT_MIN 宏定义 int最大最小值
- 静态区和全局区在一起
学习方法
- 先明确思路,再写代码,先检查代码再提交
- 学一个知识要知道为什么有这个知识点,他能干什么。
- 讲知识点从定义、原理、用法说
引用
作用:变量起别名
语法:数据类型 &
注意:必须初始化,不能初始化为空;不可以返回局部变量的引用
- 引用可以做函数参数:
示例:
//1. 值传递
void Swap01(int a, int b) {
int temp = a;
a = b;
b = temp;
}
//2. 地址传递
void Swap02(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
//3. 引用传递
void Swap03(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 10;
int b = 20;
mySwap01(a, b);
cout << "a:" << a << " b:" << b << endl;
mySwap02(&a, &b);
cout << "a:" << a << " b:" << b << endl;
mySwap03(a, b);
cout << "a:" << a << " b:" << b << endl;
return 0;
}
- 引用做函数返回值
注意:不要返回局部变量引用
用法:函数调用作为左值
示例:
#include<iostream>
using namespace std;
//返回局部变量引用
int& test01() {
int a = 10; //局部变量
return a;
}
int main() {
//不能返回局部变量的引用
int& ref = test01();
cout << "ref = " << ref << endl;
cout << "ref = " << ref << endl;
return 0;
}
本质:引用的本质在c++内部实现是一个指针常量
引用相当于给变量取一个别名,引用的本质时一个指针常量,引用必须初始化且不能初始化为空,左值引用:只能接受左值,右值引用: 只能接受右值, 万能引用: 左右值都可接收, 引用关系不能更改,引用可以做函数参数和函数返回值,函数调用做左值必须返回引用。不要返回局部变量引用。
左值右值
- 左值:有名字的可以通过名字找到地址
- 右值:没名字找不到地址的
- 左值引用: int &a; 只能接受左值
- 右值引用: int &&a; 只能接受右值
- 万能引用: const int &a; 左右值都可接收, const int &&a;不是万能引用
函数做左值必须返回引用
示例:
//返回静态变量引用
int& test02() {
static int a = 20;
return a;
}
int main() {
int a = 10;
int &b = a;
cout << "a = " << a << endl; //10
cout << "b = " << b << endl; //10
b = 100;
cout << "a = " << a << endl; //100
cout << "b = " << b << endl; //100
/*有名字的变量是左值例如:a = 1 ,a为左值,例如: 2 ,2 是右值
左值引用只能接收左值,右值引用只能接收右值*/
int &&R = 2;//右值引用接收右值
int a = 2;
int &R1 = a; //左值引用只能接收左值
//int &&R2 = a; //报错
//int &R3 = 2; //报错
const int &&R5 = 3;
//如果函数做左值,那么必须返回引用
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
test02() = 1000;
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
//万能引用:前面用const 修饰
const int &R4 = 2;
return 0;
}
值传递和引用传递
值传递:
形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的 (实参->形参),参数的值只能传入,不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。指针传递参数本质上是值传递的方式,它所传递的是一个地址值。
引用传递:
形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
匿名对象
C++的匿名对象是指在使用对象时,没有为其命名而直接创建的临时对象。匿名对象通常用于简化代码,避免显式定义一个临时对象的变量。
匿名对象的创建方式是在对象类型后面直接调用构造函数,并且没有将其赋值给一个变量。例如:
MyClass obj; // 命名对象的创建方式
MyClass(); // 匿名对象的创建方式
匿名对象可以在表达式中使用,作为函数的参数或返回值,或者赋值给其他对象。例如:
void printObject(const MyClass& obj) {
// 打印对象的内容
}
MyClass getObject() {
// 返回一个对象
return MyClass();
}
int main() {
// 使用匿名对象作为函数参数
printObject(MyClass());
// 使用匿名对象赋值给其他对象
MyClass obj = MyClass();
// 使用匿名对象作为函数返回值
MyClass newObj = getObject();
return 0;
}
产生匿名对象的三种情况:
以值的方式给函数传参;
Cat(); —> 生成了一个匿名对象,执行完Cat( )代码后,此匿名对象就此消失。这就是匿名对象的生命周期。
Cat cc = Cat(); —>首先生成了一个匿名对象,然后将此匿名对象变为了cc对象,其生命周期就变成了cc对象的生命周期。- 类型转换;
- 函数需要返回一个对象时;return temp;
匿名对象只存在于当前行
参数默认值
在C++中,函数的参数可以有默认值。当函数被调用时,如果某个参数没有明确指定值,那么就会使用默认值。这使得我们可以在函数定义中为某些参数提供默认行为,而无需为每个可能的调用都编写特定的代码。
默认参数可以放在函数声明或者定义中,但只能放在二者之一
示例
#include <iostream>
using namespace std;
void greet(string name = "World") {
cout << "Hello, " << name << "!" << endl;
}
int main() {
greet(); // 输出 "Hello, World!"
greet("Alice"); // 输出 "Hello, Alice!"
return 0;
}
在这个例子中,greet
函数有一个参数name
,它的默认值是"World"。所以当我们调用greet()
时,name
参数就会使用默认值"World"。但是如果我们给name
参数提供一个值,比如greet("Alice")
,那么这个值就会覆盖默认值。
#include <iostream>
using namespace std;
void func(int a, int b = 0, int c = 0){
cout << a << " " << b << " " << c << endl;
}
int main()
{
func(1); // 1 -> a
func(1, 2); // 1 -> a, 2 -> b
func(1, 2, 3); // 1 -> a, 2 -> b, 3 -> c
// 1 0 0
// 1 2 0
// 1 2 3
}
C++也支持多参数的默认值。例如:
void func(int a = 1, int b = 2, int c = 3) {
// ...
}
如果我们对func()
的调用没有提供任何参数,那么a
就会使用默认值1,b
会使用默认值2,c
会使用默认值3。如果我们提供了部分参数,那么未提供的参数就会使用默认值。例如,func(1)
会使得a
为1,b
为2,c
为3。
注意
从右向左给默认值,并且不可以中断。
函数重载
作用:函数名可以相同,提高复用性
函数重载满足条件:
- 同一个作用域下
- 函数名称相同
- 函数参数类型不同 或者 个数不同 或者 顺序不同
- 注意: 函数的返回值不可以作为函数重载的条件
函数重载注意事项
- 引用作为重载条件
函数重载碰到函数默认参数
示例:
#include <iostream>
using namespace std;
void fun(int a){cout << "int" << endl;}
//int fun(int a){cout << "int" << endl;} //报错 返回值不能作为重载的条件
int fun(int a, int b){cout << "int int" << endl; return 0;}
int fun(double a, int b){cout << "double int" << endl; return 0;}
int fun(float a, int b){cout << "float int" << endl; return 0;}
//int fun(int& b){cout << "int&" << endl; return 0;} // 左值引用
//int fun(int&& b){cout << "int&" << endl; return 0;} // 右值引用
//int fun(const int& b){cout << "int&" << endl; return 0;} // 万能引用
int main(){
fun(1); // int
fun(1, 2); //int int
fun(1.1, 1); //double int
fun(1.1f, 1); //float int
}
//--------
int fun(int& b){cout << "int&" << endl; return 0;}
int fun(int&& b){cout << "int&&" << endl; return 0;}
int fun(const int& b){cout << "const int&" << endl; return 0;}
int main(){
int a = 0;
fun(a); //int&
fun(1); // int&&
}
原理
编译器为了实现函数重载,默认为我们做了一些幕后的工作,编译器用不同的参数类型来修饰不同的函数名
总结
函数重载是指在同一作用域内,可以定义多个函数具有相同名称但参数列表不同的特性。返回值不可以作为重载的条件,C++通过函数的参数类型、参数个数和顺序来区分不同的重载函数。在调用重载函数时,编译器会根据传入的参数类型、个数和顺序来确定调用哪个重载函数。C++中的函数在编译之后函数名是 定义的函数名 + 参数类型,c语言函数编译后名字和定义相同,所以c语言没有函数重载,extern "C"可以让编译器以C语言风格编译。函数重载是静态多态。函数重载和参数默认值一起使用时要注意避免产生二义性的问题。
静态多态
静态多态,是指在编译期间实现的多态。
面向对象
- 面向对象的三大特性为:
封装、继承、多态
C++认为
万事万物都皆为对象
,对象上有其属性和行为
例如:- 人可以作为对象,属性有姓名、年龄、身高、体重...,行为有走、跑、跳、吃饭、唱歌...
- 车也可以作为对象,属性有轮胎、方向盘、车灯...,行为有载人、放音乐、放空调...
- 具有相同性质的
对象
,我们可以抽象称为类
,人属于人类,车属于车类
封装的意义
封装是C++面向对象三大特性之一
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
封装意义:
在设计类的时候,属性和行为写在一起,表现事物
示例:
设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
class student{
public:
int m_No;
char m_name[8];
void set_No(int No){
m_No = No;
}
void set_name(const char* name){
strcpy(m_name, name);
}
void display_info(){
cout << m_name << ' ' << m_No << endl;
}
};
成员函数存储在代码段
this指针
一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。
this是指向当前对象的指针,哪个对象调用包含this指针的函数,this指向哪个对象。
this一般在构造函数中使用,用来区分成员变量和参数。
this是所有成员函数的隐藏参数。
在C++中,当成员函数中某个变量与成员变量名字相同,则使用this关键字来表示成员变量。
或者,需要返回类变量或者结构体变量的时候,使用this关键字。
示例:
#include <iostream>
#include <string>
using namespace std;
class student{
public:
string m_name;
string m_no;
void set_name(string name){
this->m_name = name; // 通过this指针访问当前对象的n_name
}
void set_no(string no){
this->m_no = no; // 通过this指针访问当前对象的m_no
}
void display(){
cout << m_name << ' ' << m_no;
};
int main(){
student s1;
s1.set_name("pointer");
s1.set_no("001");
s1.display();
return 0;
}
三种访问权限
三种权限
- 公共权限
public
类内可以访问 类外可以访问 - 保护权限
protected
类内可以访问 类外不可以访问 私有权限
private
类内可以访问 类外不可以访问类的默认访问权限是私有的
示例:
class Person
{
//姓名 公共权限
public:
string m_Name; // 类内 子类 类外可以访问
//汽车 保护权限
protected:
string m_Car; // 类内 子类 可以访问 类外不可以访问
//银行卡密码 私有权限
private:
int m_Password; // 类内 子类 类外不可以访问
public:
void func()
{
m_Name = "张三";
m_Car = "拖拉机";
m_Password = 123456;
}
};
int main() {
Person p;
p.m_Name = "李四";
//p.m_Car = "奔驰"; //保护权限类外访问不到
//p.m_Password = 123; //私有权限类外访问不到
return 0;
}
成员变量设置为私有(private
)的好处
- 加强程序代码的安全性:通过将成员变量设置为私有,可以限制对其的访问,只允许通过类的公共方法(public methods)进行访问和操作,从而提高了程序的安全性。
- 提高封装性:封装是面向对象编程的一个核心特性,它允许隐藏对象的内部状态并仅通过对象提供的方法来访问它。将成员变量设置为私有,可以确保只有类的内部方法可以访问和修改这些变量,从而提高了封装的程度。
- 更加精确地控制成员变量的访问:私有成员变量只能通过类的公共方法进行访问和修改,这使得我们可以更加精确地控制对成员变量的访问,根据需要实现“不准访问”、“只读访问”、“只写访问”、“读写访问”。
- 方便进行修改和扩展:如果将成员变量设置为私有,日后如果想改变类的内部实现细节,比如替换成员变量,只需要修改相应的公共方法即可,而不会影响到其他使用该类的代码。
class & struct 的区别
在C++中 struct
和class
唯一的区别就在于 默认的访问权限不同和默认继承权限不同
struct
默认权限为公共class
默认权限为私有
构造函数
格式:类名(){函数体} --- Person(){...}
构造函数概念
一个类的对象被创建的时候,编译系统对象分配内存空间,并自动调用该构造函数,由构造函数完成成员变量的赋值。构造函数是给对象初始化或给成员变量赋值的
构造函数的特点
- 名字与类名相同,可以有参数,但是不能有返回值(连void也不行)。
- 构造函数是在实例化对象时自动执行的,不需要手动调用。
- 作用是对对象进行初始化工作,如给成员变量赋值等。
- 如果定义类时没有写构造函数,系统会生成一个默认的无参构造函数,默认构造函数没有参数,不做任何工作。
- 如果定义了构造函数,系统不再生成默认的无参构造函数。定义了拷贝构造或移动构造,编译器不提供构造函数和赋值运算符重载。
- 对象生成时构造函数自动调用,对象一旦生成,不能在其上再次执行构造函数
一个类可以有多个构造函数,为重载关系
构造函数的分类
- 按参数种类分为:无参构造函数、有参构造函数、有默认参构造函数
- 按类型分为:普通构造函数、拷贝构造函数(赋值构造函数)
构造函数调用规则
默认情况下,c++编译器至少给一个类添加3个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝(浅拷贝)
- 移动构造
- 重载赋值运算符(拷贝)
重载赋值运算符(移动)
构造函数调用规则如下:
- 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
拷贝构造
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象,赋值不调用(调用赋值运算符重载)。
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
示例
class Person {
public:
Person() {
cout << "无参构造函数!" << endl;
mAge = 0;
}
Person(int age) {
cout << "有参构造函数!" << endl;
mAge = age;
}
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
mAge = p.mAge;
}
//析构函数在释放内存之前调用
~Person() {
cout << "析构函数!" << endl;
}
public:
int mAge;
};
//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
Person man(100); //p对象已经创建完毕
Person newman(man); //调用拷贝构造函数
Person newman2 = man; //拷贝构造
//Person newman3;
//newman3 = man; //不是调用拷贝构造函数,赋值操作
}
//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
Person p; //无参构造函数
doWork(p);
}
//3. 以值方式返回局部对象
Person doWork2()
{
Person p1;
cout << (int *)&p1 << endl;
return p1;
}
void test03()
{
Person p = doWork2();
cout << (int *)&p << endl;
}
int main() {
Pointer p1; //无参构造函数
Pointer p2(3); //有参构造函数
Pointer p3(); //编译器会认为是函数声明
return 0;
//-------------------------
//test01();
//test02();
test03();
return 0;
}
#include <iostream>
using namespace std;
class Person{
public:
int a = 0;
Person(int a){
this->a = a;
cout << "int构造" << endl;
}
Person(const Person& other){
cout << "拷贝构造" << endl;
}
Person(){
cout << "无参构造" << endl;
}
};
int main(){
Person p1(2);//int构造
Person p2 = p1;//拷贝构造
Person p3;
p3 = p1; // 按位复制,不会调用拷贝构造
}
拷贝构造参数是引用是为了防止无限调用拷贝构造, const 为了防止修改源对象
移动构造
C++中的移动构造函数是C++11引入的一个重要特性,它允许对象的资源(如堆上的内存、文件句柄等)在不进行深拷贝的情况下从一个对象“移动”到另一个对象,从而提高了程序的性能和效率。
移动构造函数通常用于在对象的生命周期中进行资源的转移,它通过使用右值引用(Rvalue reference)来实现。移动构造函数的典型形式如下:
class MyClass {
public:
// 移动构造函数
MyClass(MyClass&& other) {
// 将资源从other对象转移到当前对象
// ...
}
};
在这里,MyClass&&
表示一个右值引用,它允许我们将资源从other
对象“窃取”而不是进行深层拷贝。通常情况下,移动构造函数会将other
对象中的资源指针置空,从而避免资源被多次释放。
使用移动构造函数可以避免不必要的资源拷贝,特别是对于大型对象或者拥有大量资源的对象,这在提高程序性能和降低内存开销方面非常有益。移动构造函数通常与右值引用和移动语义一起使用,以实现对临时对象的高效处理。
使用移动构造函数时,需要注意避免资源泄漏和悬空指针等问题,通常需要在移动资源后将原对象的资源指针置空或进行其他必要的清理操作。
移动构造函数是C++中用于实现高效资源转移的重要特性,它通过右值引用和移动语义提高了程序的性能和效率。
右值实际上是由移动构造接收的而不是拷贝构造。
std::move()可将左值转换为右值
构造函数三种调用方式:
- 括号法
- 显示法
- 隐式转换法
示例代码
//调用有参的构造函数
void test02() {
//2.1 括号法,常用
Person p1(10);
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//Person p2();
//2.2 显式法
Person p2 = Person(10);
Person p3 = Person(p2);
//Person(10)单独写就是匿名对象 当前行结束之后,马上析构
//2.3 隐式转换法
Person p4 = 10; // Person p4 = Person(10);
Person p5 = p4; // Person p5 = Person(p4);
}
explicit
在C++中,explicit
关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。
使用explicit
关键字可以避免一些意外的类型转换,提高代码的可读性和安全性。它可以防止编译器在某些情况下自动调用构造函数,从而避免了可能引起错误或意想不到的行为的隐式转换。
只适合一个参数的或者有参数默认值可以只填一个参数的构造函数
析构函数
格式: ~类名(){函数体} --- ~Person(){...}
主要作用:
在于对象销毁前系统会自动调用,进行一些清理工作。【收回创建对象时申请的空间】
- 析构函数无返回值也不写void,并且没有函数类型;
- 析构函数的函数名称要与类名相同;
- 析构函数没有参数列表,不可以进行函数重载;【构造函数有参数列表,并且可以进行重载】
- 析构函数在对象销毁时会自动调用,不需要进行手动调用,并且只调用一次。
示例
#include <iostream>
using namespace std;
class Pointer{
int *p = NULL;
public:
Pointer(){
cout << "无参构造函数" << endl;
}
Pointer(int n){
if(n > 0){
p = new int[n];
cout << "有参数构造函数" << endl;
}
}
~Pointer(){
cout << "析构函数" << endl;
if(p){
delete[] p;
}
}
};
int main(){
{
Pointer p1;
Pointer p2(3);
}
Pointer * p4 = new Pointer(1);
delete p4;
// 输出
// 无参构造函数
// 有参构造函数
// 析构函数
// 析构函数
// 有参构造函数
// 析构函数
return 0;
}
初始化列表
- 初始化参数列表只能在构造函数中使用
- 初始化的顺序和初始化参数列表的顺序无关,和变量声明的顺序一致
- 常量和引用必须在初始化参数列表中初始化
初始化参数列表用于在构造函数中对类的成员变量进行初始化。初始化参数列表位于构造函数的参数列表之后,使用冒号(:)分隔。
初始化参数列表的作用包括:
- 效率:通过初始化参数列表初始化成员变量可以避免先调用默认构造函数再赋值的开销,提高效率。
- 对 const 和引用类型成员变量的必要性:对于 const 成员变量或引用类型成员变量,必须在初始化参数列表中进行初始化。
- 初始化顺序:成员变量的初始化顺序只与它们在类中声明的顺序有关。
示例
class A{
public:
int a, b, c;
A(int a1, int b1): b(a1), a(b1), c(a) //与这里的顺序无关,只与定义成员变量的顺序有关
{
cout << a << ' ' << b << ' ' << c << endl;
}
};
int main(){
A(3, 1); // 1 3 1
return 0;
}
总结
初始化参数列表是在构造函数中使用冒号(:)后跟随成员变量初始化的语法结构。通过初始化参数列表初始化成员变量可以避免先调用默认构造函数再赋值的开销,提高效率。对于 const 成员变量或引用类型成员变量,必须在初始化参数列表中进行初始化。成员变量的初始化顺序只与它们在类中声明的顺序有关。
深拷贝 & 浅拷贝
在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的,但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当一个对象析构时,会调用析构函数,从而导致野指针,所以,此时,必须采用深拷贝。
在 C++ 中,深拷贝(deep copy)和浅拷贝(shallow copy)是与对象的复制相关的概念。这些概念通常与类和对象的复制构造函数、赋值运算符重载以及动态内存分配相关。
浅拷贝(Shallow Copy)
浅拷贝是指在对象复制时,仅仅复制对象中的成员变量的值,如果成员变量中有指针,则复制的是指针的值,而不是指针指向的内容。这意味着原始对象和副本对象将共享相同的内存地址。当一个对象被销毁时,它所指向的内存将被释放,而另一个对象仍然指向这片内存,可能导致悬空指针或者二次释放内存的问题。
深拷贝(Deep Copy)
深拷贝是指在对象复制时,不仅复制对象中的成员变量的值,而且对于成员变量中的指针,还要复制指针指向的内容。这样,原始对象和副本对象将拥有各自独立的内存空间,彼此之间不会相互影响。
示例
#include <iostream>
using namespace std;
class Person {
public:
Person() {
cout << "无参构造函数!" << endl;
n = 0;
}
Person(int n) {
cout << "int构造函数!" << endl;
if(n > 0){
this->n = n;
p = new int[n];
}
}
Person(const Person& other) {
cout << "拷贝构造函数!" << endl;
//this->n = other.n;
// this->p = other.p // 浅拷贝,编译器默认提供的就是这种
this->n = other.n;
p = new int[n];
for(int i = 0; i < n; i++){
this->p[i] = other.p[i];
} //深拷贝
}
~Person() {
cout << "析构函数!" << endl;
}
public:
int n = 0;
int *p;
};
int main() {
Person p(5); // int构造函数
Person p1 = p; //调用拷贝构造进行深拷贝
return 0;
}
总结
浅拷贝:
- 浅拷贝是指在对象拷贝时,只是简单地复制对象的成员变量的值。如果对象中包含指针,则浅拷贝只会复制指针的值,而不会复制指针指向的数据。这意味着如果原对象和副本对象共享相同的资源,当其中一个对象的资源释放后,另一个对象可能会访问到无效的内存,从而导致错误。
- 默认情况下,C++类的拷贝构造函数和赋值操作符执行的是浅拷贝。
深拷贝:
- 深拷贝是指在对象拷贝时,会复制对象所指向的所有数据,包括动态分配的内存。这样可以避免多个对象共享同一块内存区域的问题,每个对象都有自己独立的内存空间。
- 为了实现深拷贝,通常需要自定义拷贝构造函数和赋值操作符,以确保在对象拷贝时,所有动态分配的资源都得到正确的复制。
类对象作为类成员
class Phone{
string name;
public:
Phone(string pname):name(pname)
{
cout << "Phone构造" << endl;
}
~Phone(){
cout << "Phone析构" << endl;
}
};
class Person{
string m_name;
Phone m_phone;
public:
Person(string name, string phoneName): m_name(name), m_phone(phoneName)
{
cout << "person构造" << endl;
}
~Person(){
cout << "person析构" << endl;
}
};
int main(){
Person("张三", "iphone");
//Phone构造
//person构造
//person析构
//Phone析构
return 0;
}
创建对象时先执行成员对象的构造函数, 在执行当前类的构造函数, 对象销毁时,先调用当前类的析构函数, 再调用成员对象的析构函数。
静态成员
静态成员分为:
静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存,在主函数之前进行构造
- 类内声明,类外初始化
- 在发生继承时,静态成员变量不会被继承,父类子类共享同一个静态成员
- 可以使用类或对象访问公有的静态成员变量
- 存储在静态区,不占用sizeof(类)的空间,不占用对象的内存
示例
#include<iostream>
#include<string>
using namespace std;
class A
{
public:
A() { cout << "A构造" << endl; }
};
class phone
{
string name;
public:
static A a;
static int n;//静态区,静态成员变量属于类不属于对象,所有的对象共享同一个静态成员变量
phone(){}
phone(string name)
{
this->name = name;
cout << "phone构造" << endl;
}
~phone()
{
cout << "phone析构" << endl;
}
};
//静态成员变量必须在类外初始化
int phone::n = 0;
A phone::a = A(); // 在主函数之前进行构造,即使不创建phone类型的对象也会构造
int main() {
A b;
phone::a = b; //不创建对象也能访问静态成员变量,静态成员变量属于类不属于对象,所有对象共享同一个静态成员
phone p,p1;
p.n++;
p1.n++;
//可以通过变量名或类名 直接访问公有的静态成员变量
cout << p.n << endl; // 2
phone::n++;
cout << phone::n << endl; //3
return 0;
}
- 静态成员函数
- 属于类不属于对象,所有对象或作用域都可以访问共有的静态成函数
- 静态成员函数没有this指针,静态成员变量不能访问非静态成员
- 存储在代码段
示例代码
class Person
{
public:
//静态成员函数特点:
//1 程序共享一个函数
//2 静态成员函数只能访问静态成员变量
static void func()
{
cout << "func调用" << endl;
m_A = 100;
//m_B = 100; //错误,不可以访问非静态成员变量
}
static int m_A; //静态成员变量
int m_B; //
private:
//静态成员函数也是有访问权限的
static void func2()
{
cout << "func2调用" << endl;
}
};
int Person::m_A = 10;
void test01()
{
//静态成员变量两种访问方式
//1、通过对象
Person p1;
p1.func();
//2、通过类名
Person::func();
//Person::func2(); //私有权限访问不到
}
int main() {
test01();
return 0;
}
总结
- 静态成员变量是属于类的变量,而不是属于类的实例。它在内存中只有一份拷贝,被所有类的实例共享,在类的内部定义,在类的外部初始化,并且可以直接通过类名访问,也可以通过类的实例访问。通常用于表示类相关的全局数据或共享数据。
- 静态成员函数是不依赖于类的实例,属于类本身的函数。没有this指针,它不能访问非静态成员变量或非静态成员函数,因为它们不与特定的类实例相关联。可以直接通过类名调用,不需要创建类的实例。通常用于处理与类相关的全局操作或管理类的静态数据,并且它的代码通常存储在程序的代码段中,因为它们不依赖于特定的类实例。
空指针访问成员函数
C++中空指针也是可以调用成员函数的,但是也要注意成员函数有没有用到this指针。
如果用到this指针,需要加以判断保证代码的健壮性
常对象 & 常函数
- 成员函数后加const后称为这个函数为常函数
- 常函数内不可以修改成员属性,也不可以调用非常函数
- 成员属性声明时加关键字mutable后,在常函数中可以修改
- 如果两个函数名字相同参数相同,但是一个是常函数一个非常函数,那么这两个函数是函数重载,非常对象优先调用非常函数,常对象只能调用常函数
- 可以改变静态成员变量和调用静态成员函数
常对象:
- 常对象意味着成员变量不可以修改,mutable修饰成员变量除外
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数不可以调用非常函数
mutable
mutable 的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。在C++中,mutable也是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
总结
常函数是指在成员函数后加上const关键字,这意味着在常函数内部不可以修改成员变量,也不可以调用非常函数。然而,如果在成员变量声明时加上关键字mutable,那么在常函数中可以修改这些属性。此外,如果存在两个函数名字相同、参数相同,但一个是常函数,一个是非常函数,那么这两个函数构成函数重载。在这种情况下,非常对象会优先调用非常函数,而常对象只能调用常函数。
常对象意味着其成员变量不可修改,但如果成员变量被mutable修饰,则可以在常对象中修改它们。声明对象前加上const关键字可以将该对象定义为常对象,而常对象只能调用常函数,而不能调用非常函数。
友元
友元的目的就是让一个函数或者类 访问另一个类中私有成员
友元的关键字为 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
在C++中,友元(friend)是一种特殊的关系,允许一个类或函数访问另一个类的私有成员。这种关系允许一个类或函数在没有继承关系的情况下访问另一个类的私有成员。友元关系在类定义中进行声明。
在类定义中,可以将其他类或函数声明为友元,使它们能够访问当前类的私有成员。友元声明的语法如下:
class A {
public:
friend class B; // 类B是类A的友元
friend void functionX(); // 函数functionX是类A的友元
friend void C::func(); // 类C的函数func是类A的友元
private:
int privateMember;
};
在上面的例子中,类B和函数functionX被声明为类A的友元,因此它们可以访问类A的私有成员privateMember。
需要注意的是,友元关系是单向的。如果类B被声明为类A的友元,那么类A的成员函数可以访问类B的私有成员,但类B的成员函数并不能访问类A的私有成员,除非类A也将类B声明为友元。
总结
友元允许一个类或函数访问另一个类的私有成员。友元关系可以在类定义中进行声明。通过友元关系,一个类可以将其他类或函数声明为友元,使它们能够访问当前类的私有成员。友元关系是单向的。坏处是会破坏类的封装性。
重载运算符
运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
运算符重载是一种特性,允许用户重新定义已有的运算符,使其能够用于自定义类型。通过运算符重载,可以为自定义的类类型定义适合该类对象的运算符行为,使得类对象可以像内置类型一样进行运算。
注意:
- 不能创建新的运算符,只能重载已有的运算符。
- 不能改变运算符的优先级和结合性。
- 不能重载的运算符包括成员访问运算符
.
、成员指针访问运算符.*
、作用域解析运算符::
、条件运算符?:
和逗号运算符,
、sizeof
运算符。 - 对于内置的数据类型的表达式的的运算符是不可能改变的
加号运算符重载
示例
#include<iostream>
#include<string>
using namespace std;
class Box
{
int length;
int width;
int height;
friend Box operator+ (const Box &other, const Box &other1);
friend Box operator+ (const Box &other, int val);
public:
Box()
{
length = 0;
width = 0;
height = 0;
}
Box(int length,int width,int height)
{
this->length = length ;
this->width = width ;
this->height = height ;
}
Box( const Box& other)
{
cout << "拷贝构造" << endl;
this->length = other.length;
this->width = other.width;
this->height = other.height;
}
//实现成员函数 + 运算符
//Box operator+ (const Box &other)
//{
// Box p;
// p.length = this->length + other.length;
// p.width = this->width + other.width;
// p.height = this->height + other.height;
// return p;
//}
};
//全局实现 + 运算符
Box operator+ (const Box &other ,const Box &other1)
{
Box p;
p.length = other1.length + other.length;
p.width = other1.width + other.width;
p.height = other1.height + other.height;
return p;
}
//全局实现 + 运算符
Box operator+ (const Box &other, int val)
{
Box p;
p.length = val+ other.length;
p.width = val + other.width;
p.height = val + other.height;
return p;
}
int main() {
Box a, b;
Box c = a + b;//相当于a.operator+(b);
a + 10;
return 0;
}
重载左移运算符
示例
#include <iostream>
using namespace std;
class Box {
int m_height;
int m_width;
int m_length;
friend ostream& operator<< (ostream& os, Box b); //声明友元为了访问私有成员变量
public:
Box() : m_height(1), m_width(1), m_length(1) {
}
};
ostream& operator<< (ostream& os, Box b){ // 返回ostream&目的是为了链式调用
os << b.m_length << ' ' << b.m_width << ' ' << b.m_height;
return os;
}
int main() {
Box a, b, c;
cout << a << b << c;
return 0;
}
重载左移运算符配合友元可以实现输出自定义数据类型
重载+=运算符
示例
#include <iostream>
using namespace std;
class Box {
int m_height;
int m_width;
int m_length;
friend Box& operator+=(Box& a, const Box& b);
public:
Box() : m_height(1), m_width(1), m_length(1) {
}
};
Box& operator+=(Box& a, const Box& b){ //不可以使用万能引用,因为万能引用不可以修改a的值
a.m_length += b.m_length;
a.m_height += b.m_height;
a.m_width += b.m_width;
return a;
}//如果想实现既接收左值又接受右值,可以再实现一个接受右值的重载
int main() {
Box a, b, c;
a += b += c;
cout << a << endl;
cout << b << endl;
cout << c << endl;
// 输出 从右往左计算
// 3 3 3
// 2 2 2
// 1 1 1
return 0;
}
重载自增运算符
示例
#include <iostream>
using namespace std;
class MyInteger{
int num;
friend ostream& operator<<(ostream& out, const MyInteger& other);
public:
MyInteger(int a){
num = a;
}
MyInteger& operator++() //前++
{
this->num++;
return *this;
}
MyInteger operator++(int){ //后++
MyInteger temp = *this;
this->num++;
return temp;
}
};
ostream& operator<<(ostream& out, const MyInteger& other){
out << other.num ;
return out;
}
int main() {
MyInteger a = 1;
MyInteger b = 2;
cout << (a <= b) << endl; // 1
cout << (a == b) << endl; // 0
}
关系运算符
示例
#include <iostream>
using namespace std;
class MyInteger{
int num;
friend bool operator==(const MyInteger& a, const MyInteger& b);
public:
MyInteger(int a){
num = a;
}
// bool operator==(const MyInteger& other){ //类内重载==运算符
//
// return this->num == other.num;
// }
bool operator<=(const MyInteger& other){ //类内重载<=运算符
return this->num <= other.num;
}
};
bool operator==(const MyInteger& a, const MyInteger& b){ //类内重载==运算符
return a.num == b.num;
}
int main() {
MyInteger a = 1;
MyInteger b = 2;
cout << (a <= b) << endl; // 1
cout << (a == b) << endl; // 0
}
赋值运算符重载
编译器赋值运算符重载,自带的是浅拷贝
给已经存在的对象赋值,调用赋值运算符,用已经存在的对象初始化新对象调用的是拷贝构造
示例
#include <iostream>
using namespace std;
class MyInteger{
int num;
friend bool operator==(const MyInteger& a, const MyInteger& b);
public:
MyInteger(int a){
num = a;
}
bool operator<(const MyInteger& other){ //类内重载<=运算符
return this->num < other.num;
}
MyInteger& operator=(const MyInteger& other){
this->num = other.num;
return *this;
}
};
bool operator==(const MyInteger& a, const MyInteger& b){ //类内重载==运算符
return a.num == b.num;
}
int main() {
MyInteger a = 1;
MyInteger b = 2;
b = a;//调用赋值运算符重载
MyInteger c = a; //调用拷贝构造
cout << (a < b) << endl; // 0
cout << (a == b) << endl; // 1
}
仿函数
C++中仿函数是一个能行使函数功能的类。
重载函数调用操作符的类,其对象常称为函数对象(function object),也叫仿函数(functor),使得类对象可以像函数那样调用。
STL提供的算法往往有两个版本,一种是按照我们常规默认的运算来执行,另一种允许用户自己定义一些运算或操作,通常通过回调函数或模版参数的方式来实现,此时functor便派上了用场,特别是作为模版参数的时候,只能传类型。
函数对象超出了普通函数的概念,其内部可以拥有自己的状态(其实也就相当于函数内的static变量),可以通过成员变量的方式被记录下来。
函数对象可以作为函数的参数传递。
函数对象通常不定义构造和析构函数,所以在构造和析构时不会发生任何问题,避免了函数调用时的运行时问题。
模版函数对象使函数对象具有通用性,这也是它的优势之一。
STL需要我们提供的functor通常只有一元和二元两种。
lambda 表达式的内部实现其实也是仿函数
关系运算类函数对象
template<class T> bool equal_to<T>; // 等于
template<class T> bool not_equal_to<T>; // 不等于
template<class T> bool greater<T>; // 大于
template<class T> bool greater_equal<T>; // 大于等于
template<class T> bool less<T>; // 小于
template<class T> bool less_equal<T>; // 小于等于
示例
#include <iostream>
using namespace std;
class MyInteger{
int num;
public:
MyInteger(int a){
num = a;
}
int operator()(int a){ //重载()运算符
cout << a;
}
};
int main() {
MyInteger a = 1;
a(1); //输出1
}
继承
继承是指一个对象(子类)可以继承另一个对象(父类)的属性和方法,从而可以重用父类的代码并扩展其功能。通过继承,可以建立对象之间的层次关系,提高代码的复用性和可维护性。
基类和派生类:
- 基类(父类)是被继承的类,它定义了一组属性和行为。
- 派生类(子类)是从基类继承属性和行为的类,并且可以添加新的属性和行为。
访问控制:
- C++ 中有三种继承方式:公有继承、保护继承和私有继承。作用是缩小父类成员在子类中对外的访问权限,小于或等于继承方式的成员访问权限不变,访问权限大于继承方式的权限会被缩小到继承方式的权限
- 公有继承(public inheritance):基类的公有成员在派生类中仍然是公有的,保护成员在派生类中是保护的,私有成员在派生类中是不可访问的。
- 保护继承(protected inheritance):基类的公有和保护成员在派生类中都变成保护的,私有成员在派生类中是不可访问的。
- 私有继承(private inheritance):基类的公有和保护成员在派生类中都变成私有的,私有成员在派生类中是不可访问的。
多继承:
- C++ 支持多重继承,即一个派生类可以同时从多个基类继承属性和行为。
- 多继承导致二义性问题可以使用作用域解决。
菱形继承
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
- 利用虚继承或作用域可以解决菱形继承问题
虚函数和多态:
- 虚函数允许在派生类中重写基类的函数,从而实现运行时多态性。
- 通过基类指针或引用调用派生类对象的虚函数时,根据实际对象类型选择调用的函数,这就是多态性的体现。
构造函数和析构函数:
- 派生类的构造函数可以调用基类的构造函数,确保基类部分被正确初始化。
- 析构函数会按照相反的顺序被调用,即先派生类的析构函数,再基类的析构函数。
友元和继承:
- 友元关系不会被继承,即基类的友元对于派生类不可见。
当父类没有无参构造时,要在子类初始化参数列表中显式调用父类构造函数。当成员变量是类对象时,可以在初始化参数列表中调用该对象的构造函数。
访问权限示例
#include <iostream>
class A{
private:
int pri;
protected:
int pro;
public:
int pub;
};
class B : private A{
public:
void fun(){
//pri = 1; 报错,只能在类A中访问
pro = 1;
pub = 1;
}
};
class C : protected A{
public:
void fun(){
//pri = 1; 报错,只能在类A中访问
pro = 1; //本类和子类可访问
pub = 1; //本类和子类可访问
}
};
class E : public A{
public:
void fun(){
//pri = 1; 报错,只能在类A中访问
pro = 1; //本类和子类可访问
pub = 1; //本类和子类可访问
}
};
class D : private C{
public:
void fun1(){
//pri = 1; 报错,只能在类A中访问
pro = 1; //本类和子类可访问
pub = 1; //本类和子类可访问
}
};
int main(){
B b; // private 继承
// b.pri;
// b.pro; //类外均不可访问
// b.pub;
C c; // protected 继承
// c.pri;
// c.pro; //类外均不可访问
// c.pub;
D d; //c子类
// d.pri;
// d.pro; //类外均不可访问
// d.pub;
E e; // public 继承
// e.pri; //类外不可访问
// e.pro; //类外不可访问
e.pub; //类外可访问
return 0;
}
构造析构顺序示例
#include <iostream>
using namespace std;
class Base{
public:
Base(){
cout << "Base" << endl;
}
~Base(){
cout << "~Base" << endl;
}
};
class Son : public Base{
public:
Son(){
cout << "Son" << endl;
}
~Son(){
cout << "~Son" << endl;
}
};
class GrandSon : public Son{
public:
GrandSon(){
cout << "GrandSon" << endl;
}
~GrandSon(){
cout << "~GrandSon" << endl;
}
};
int main(){
GrandSon g;
//Base
//Son
//GrandSon
//~GrandSon
//~Son
//~Base
return 0;
}
同名函数示例
#include <iostream>
using namespace std;
class Base{
public:
void fun(){
cout << "base fun void" << endl;
}
void fun(int){
cout << "base fun int" << endl;
}
};
class Son : public Base{
public:
void fun(int){
cout << "son fun int" << endl;
}
};
int main(){
Son s;
s.fun(1); // son fun int ,这种方式只能访问子类中的fun(), 父类中的同名fun()全部被隐藏了
s.Base::fun(); // base fun void , 可以通过作用域访问父类中的同名fun()
return 0;
}
共享静态成员示例
#include <iostream>
using namespace std;
class Base{
public:
static int a;
};
class Son : public Base{
public:
};
int Base::a = 1;
int main(){
Base::a++;
Son::a++;
cout << Base::a<< endl; //3
return 0;
}
//------------同名静态成员
#include <iostream>
using namespace std;
class Base{
public:
static int a;
};
class Son : public Base{
public:
static int a;
};
int Base::a = 1;
int Son::a = 2;
int main(){
Base::a++;
Son::a++;
cout << Base::a<< endl; //2
cout << Son::a<< endl; //3
cout << Son::Base::a<< endl; //2
return 0;
}
菱形继承
菱形继承概念:
两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承
#include <iostream>
using namespace std;
class A {
public:
int a;
A()
{
cout << "A构造" << endl;
}
};
class B : virtual public A {
public:
B()
{
cout << "B构造" << endl;
}
};
class C : virtual public A {
public:
C()
{
cout << "C构造" << endl;
}
};
class D : public B, public C {
};
int main()
{
D d;
d.a;
d.B::a;
return 0;
}
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
- 利用虚继承可以解决菱形继承问题
通过虚基类指针和虚基类表实现
总结
通过继承机制,一个类(派生类)可以从另一个类(基类)继承属性和行为。
基类负责定义所有类共同拥有的成员,而每个派生类定义各自特有的成员。派生类必须通过使用类派生列表明确指出它是从哪个类继承而来,并说明它是公有继承、保护继承还是私有继承。
继承权限的作用是缩小父类成员在子类中的访问权限。
在创建子类时,首先在内存中创建父类对象,然后再创建子类对象,并将它们拼接在一起。构造对象时,先根据继承列表顺序调用父类构造函数,再调用子类构造函数;在析构对象时,先调用子类析构函数,再调用父类析构函数。
当子类和父类拥有同名成员函数时,子类会隐藏父类中所有的同名成员函数。为了访问父类中的同名成员函数,可以使用作用域解析运算符,如 son.Base::fun()
。
在继承关系中,静态成员变量不会被继承。当存在同名静态成员时,需要使用作用域解析运算符来访问。
多继承导致二义性问题使用作用域解决。
多态
函数重写(覆盖)
子类重新定义父类中相同函数名、返回值和参数的虚函数,只在继承关系中出现。
基本条件:重写的函数和被重写的函数都为virtual
函数,并且分别在派生类和基类中,重写的函数和被重写的函数的返回值、函数名和参数必须一致。
要实现重写,最高级的祖先类中,返回值、名字和参数相同的函数必须为虚函数。
函数隐藏
在子类中只要和父类函数名字相同不是重写,一定是函数隐藏。
示例
#include <iostream>
using namespace std;
class A{
public:
void funA(){
cout << "funA" << endl;
}
virtual void funB(){
cout << "funB" << endl;
}
};
class B : public A{
public:
void funA() { //隐藏 不是虚函数
cout << "B::funA()" << endl;
}
void funA(int a) { //隐藏 同名参数不同
cout << "B::funA(int a)" << endl;
}
void funB(){ //重写 //子类中不加virtual关键字也是虚函数
cout << "B::funB()" << endl;
}
};
int main(){
B b;
b.funA(); // B::funA()
b.funA(1); // B::funA(int a)
b.funB(); // B::funB()
}
静态多态和动态多态
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
多态的满足条件
- 有继承关系
- 子类重写父类的虚函数
- 父类的引用或指针指向子类的对象
多态示例
#include <iostream>
using namespace std;
class Animal{
public:
virtual void speak(){
cout << "Animal speak" << endl;
}
};
class Cat : public Animal{
public:
void speak(){
cout << "Cat speak" << endl;
}
};
class Dog : public Animal{
public:
void speak(){
cout << "Dog speak" << endl;
}
};
void doSpeak(Animal& ani){
ani.speak();
}
int main(){
Cat c;
doSpeak(c); //Cat speak
Dog d;
doSpeak(d); //Dog speak
}
虚函数表存储在静态区,不占对象内存
多态的内部实现
虚函数表(不占对象内存)和虚表指针(占对象内存)
虚表指针属于对象不属于类
C++虚函数表深入探索(详细全面)-腾讯云开发者社区-腾讯云 (tencent.com)
为什么基类指针可以指向子类对象
子类转换成父类,需要压缩内存,父类转换成子类,需要扩容,不可以扩容。
当一个子类继承自一个基类时,子类会继承基类的属性和方法。因此,子类对象在内存中包含了基类对象的所有成员变量和方法,以及可能新增的自己的成员变量和方法。由于子类对象包含了基类对象的内容,所以基类指针可以指向子类对象。
纯虚函数和抽象类
纯虚函数语法:virtual 返回值类型 函数名(参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
示例
class Base
{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};
void test01()
{
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;//记得销毁
}
int main() {
test01();
system("pause");
return 0;
}
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
不使用虚析构时,如果父类指针指向子类对象,析构时只会调用父类析构函数。
(1)如果父类的析构函数不加virtual关键字
当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调用父类的析构函数,而不调用子类的析构函数。
(2)如果父类的析构函数加virtual关键字
当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调用子类的析构函数,再调用父类的析构函数。
虚析构实现原理:通过将基类的析构函数声明为虚函数,在调用析构函数时,实际上调用的是子类重写的析构函数,在子类析构函数的最后再调用父类的析构函数,这样就保证了正确的析构顺序
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
- 虚析构语法:
virtual ~类名(){}
- 纯虚析构语法:
virtual ~类名() = 0;
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
其他
空类的sizeof为1
在 C++ 中,空类的 sizeof
为 1 是由 C++标准规定的。这种情况通常被称为“空类优化”或“空基类优化”。
确保了空类的实例在内存中占据了一个字节的空间,从而保证了每个实例都有独一无二的地址
这种规定是由 C++ 标准规定的,因此不同的编译器可能会有不同的实现方式,但是在大多数情况下,空类的 sizeof
为 1。
创建对象时先调用成员变量的构造函数,再调用该类的构造函数,析构时相反
在 C++ 中,对象的构造和析构顺序与对象的成员变量声明顺序有关。当创建一个对象时,首先会调用该类的基类构造函数,然后按照成员变量的声明顺序依次调用它们的构造函数,最后调用该类自身的构造函数。在对象被销毁时,析构的顺序与构造相反:首先调用该类的析构函数,然后按照成员变量的声明顺序依次调用它们的析构函数,最后调用基类的析构函数。
这种顺序保证了在对象构造时,所有成员变量都已经被正确初始化,而在对象销毁时,所有成员变量都还处于有效状态。
这个顺序确保了对象的正确构造和销毁,尤其对于基类和成员变量之间的关系非常重要。因此,在设计和使用类时,需要特别注意成员变量的声明顺序,以确保构造和析构的正确顺序。
extern "C"
extern "C"
是 C++ 中用来声明 C 语言风格的函数接口的关键字。在 C++ 中,函数名会被编译器进行名称修饰(name mangling),以便支持函数重载和其他特性。而在 C 语言中,函数名不会被进行名称修饰。当我们需要在 C++ 代码中调用一个由 C 语言编写的函数时,就需要使用 extern "C"
来告诉编译器使用 C 语言的函数名规则,以便正确地链接和调用这些函数。
示例
例如,假设有一个 C 语言编写的函数声明如下:
// 在 C 语言中的函数声明
void myCFunction(int arg);
在 C++ 代码中,如果要调用这个函数,需要使用 extern "C"
声明:
extern "C" {
// 在 C++ 中使用 C 语言风格的函数声明
void myCFunction(int arg);
}
命名空间
C++ 的命名空间是一种用来避免命名冲突并组织代码的机制。通过命名空间,可以将一系列的标识符(比如变量、函数、类等)封装在一个特定的作用域内,从而在大型项目中更好地组织和管理代码。
以下是关于 C++ 命名空间的一些重要概念:
- 命名空间的定义:
- 使用关键字
namespace
来定义命名空间,后跟命名空间的名称和一对大括号,大括号内包含了命名空间中的内容。
namespace MyNamespace {
// 命名空间中的内容
int variable;
void function();
class MyClass {
// 类定义
};
}
命名空间的作用域:
- 命名空间可以嵌套定义,内层命名空间的名称会成为外层命名空间的子空间。
namespace OuterNamespace {
namespace InnerNamespace {
// 内层命名空间
}
}
使用命名空间中的成员:
- 在使用命名空间中的成员时,可以通过作用域解析运算符
::
来指明所属的命名空间。
- 在使用命名空间中的成员时,可以通过作用域解析运算符
MyNamespace::variable = 5; // 访问命名空间中的变量
MyNamespace::function(); // 调用命名空间中的函数
引入命名空间:
- 可以使用
using
关键字将命名空间中的部分或全部成员引入当前作用域,从而避免重复的命名空间前缀。
- 可以使用
using namespace MyNamespace; // 引入整个命名空间
variable = 5; // 直接访问命名空间中的变量
全局命名空间:
- 如果在全局作用域中定义的内容不属于任何命名空间,那么它们属于全局命名空间。
命名空间是 C++ 中组织代码、避免命名冲突的重要工具,能够帮助开发者更好地管理代码结构,并提高代码的可读性和可维护性。
参数占位符
在C++中,参数占位符通常指的是在函数签名中使用的占位符,它们用于表示函数的参数列表,但在函数定义或调用中并不直接使用这些参数。在C++中,有两种主要的参数占位符:
- 使用
void
作为参数占位符:在函数声明或定义中,使用void
作为参数列表表示函数不接受任何参数。例如:
void functionName(void);
在这种情况下,void
被用作参数占位符,表示该函数不接受任何参数。
- 使用未命名的参数作为占位符:在函数声明或定义中,可以使用未命名的参数作为占位符,表示函数接受参数,但在函数体内并不直接使用这些参数。这通常用于函数重载,例如:
void function(int); // 函数接受一个int类型的参数,但在函数体内并不使用该参数
在这种情况下,函数声明表明函数接受一个int
类型的参数,但在函数体内并不使用该参数。
这些参数占位符的使用主要是为了在函数声明中表明函数的参数列表或者在函数定义中表明函数接受参数,但在函数体内并不使用这些参数。在实际编程中,这种技术通常用于函数声明、函数重载或者遵循特定的API要求。
inline内联函数
宏函数的缺点
#include<iostream>
using namespace std;
#define ADD(x,y) ((x) + (y))
#define ADD_One(x,y) (x * y)
int main()
{
cout << ADD("asd", 1) << endl;//宏定义没有类型检查
cout << ADD_One(1+2,3+4); //宏定义有二义性
}
内联函数的定义
以 inline
修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
如果是一个几十行的函数,函数本身执行就很耗时,那调用函数、创建栈帧(每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。其实就是函数运行时开辟的函数栈。)的一两行可以忽略不计;但是如果一个函数本身就一两行,因为调用函数本身而产生的那一部分消耗,就格外突出,而宏或者内联的提前替换就显得格外优秀,提高了效率。
inline int add(int a, int b)
{
return a + b;
}
特点:
inline
是一种以空间换时间的做法,省去调用函数、建立栈帧的额外开销,但是如果代码很长(一般是10行左右,具体取决于编译器),或者有递归函数,即使函数前面声明了inline,那么编译器也不会让该函数成为内联函数。inline
对于编译器而言只是一个建议,编译器会自动优化,如果定义的函数很长或者递归函数等等,编译器优化时会忽略掉内联。inline
不可以声明和定义分离,.h文件中使用inline
声明内联函数.cpp文件中不使用inline
定义函数,会报错链接错误。
模板
模板的基本声明和定义
template <typename T> int compare (T t1, T t2);
template <typename T> class compare;
定义一个模板函数
template<typename T>
int compare(T &t1, T &t2) {
if (t1 > t2)
return 1;
if (t1 == t2)
return 0;
if (t1 < t2)
return -1;
}
定义一个模板类
template<typename T>
class compare {
private:
T _val;
public:
compare(T &val) : _val(val) {}
bool operator==(T &t) {
return _val == t;
}
};
模板实现栈
#include <iostream>
#include <vector>
using namespace std;
template <typename T>
class stack{
vector<T> buf;
public:
void pop(){
buf.pop_back();
}
void push(const T& item){
buf.push_back(item);
}
T top(){
return buf.back();
}
bool empty(){
return buf.empty();
}
};
int main() {
stack<int> stk;
for(int i = 1; i <= 5; i++){
stk.push(i);
}
while(!stk.empty()){
cout << stk.top() << " ";
stk.pop();
}
return 0;
}
模板特化
对于某种需要特殊处理的模板参数, 可以使用模板特化
模板特化是为模板提供特殊版本的一种机制。这个特殊版本仅适用于特定的模板参数。
使用场景
- 当某个特定类型需要不同的处理逻辑时。
- 优化:为特定类型提供更高效的算法。
注意事项
- 特化版本不能独立于原始模板存在。
- 不能有多个相同的特化版本。
template<typename T>
class Test {
void fun(T arg) {
cout << "fun(T arg)" << endl;
}
};
template<>
class Test<int> { // 为 Test 类提供了一个 int 特化的版本 当模板参数为int时 会使用这个实现
void fun(int arg) {
cout << "fun(int arg)" << endl;
}
};
模板偏特化是在模板的某些参数已经确定的情况下,为剩余参数提供一个特殊实现。
使用场景
- 当只有某几个模板参数需要特殊处理时。
- 针对模板参数的某些属性(比如是否为指针、是否为常量等)进行特化。
注意事项
- 偏特化只适用于类模板,不适用于函数模板。
- 偏特化版本的参数数量必须少于原始模板。
template<typename T1, typename T2>
class Test {
public:
void fun(T1 arg, T2 arg2) {
cout << "fun(T arg)" << endl;
}
};
template<typename T2> // 当第一个参数为 int 时会使用这个偏特化的版本
class Test<int, T2> {
public:
void fun(int arg) {
cout << "fun(int arg)" << endl;
}
};
异常处理
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,然后确定发生了哪方面的错误,并且异常不会终止程序。
throw
: 当问题出现时,程序会抛出一个异常。这是通过使用throw
关键字来完成的。try
: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。catch
: 在您想要处理问题的地方,通过异常处理程序捕获异常catch
关键字用于捕获异常,可以有多个catch
进行捕获- 抛出异常对象后,会生成一个异常对象的拷贝(调用拷贝构造),因为m出作用域会被销毁。
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个
catch
的处理代码。 - 被选中的处理代码的调用链是,找到于该类型匹配且离抛出异常位置最近的那一个
catch
。 - 实际中抛出和捕获的类型不一定类型完全匹配,可以抛出派生类对象,使用基类来捕获,这个在实际中很实用。主要原因是:派生类对象可以赋值给基类引用或指针。
示例
#include <iostream>
using namespace std;
class MyException{ //自定义异常类
string msg;
public:
MyException(string msg){
this->msg = msg;
}
string what(){
return msg;
}
};
int divi(int a, int b){
if(b == 0) {
throw MyException("除数为 0 !"); //抛出异常
}
return a / b;
}
int main() {
try{
divi(1, 0);
}catch(MyException& ex){ // 捕获异常
cout << ex.what() << endl;
} //输出除数为 0 !
return 0;
}
整个函数体可以作为try 块
示例
void fun() try {
throw 1;
} catch (int e) {
std::cout << "catch int" << std::endl;
} catch (...) {
std::cout << "catch ..." << std::endl;
}
在构造函数上使用
class MyClass {
public:
int a;
// 委托构造函数
MyClass(int x) :a(1) {
std::cout << "MyClass(int x) called" << std::endl;
}
MyClass(int x, int y) try : MyClass(x){
std::cout << "MyClass(int x, int y) called" << std::endl;
} catch (...) {
std::cout << "catch ..." << std::endl;
}
};
noexcept
该关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。
如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序。
使用noexcept表明函数或操作不会发生异常,会给编译器更大的优化空间。然而,并不是加上noexcept就能提高效率
以下情形鼓励使用noexcept:
- 移动构造函数(move constructor)
- 移动赋值函数(move assignment)
- 析构函数(destructor)。这里提一句,在新版本的编译器中,析构函数是默认加上关键字noexcept的。
- 叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。
原因
容器类在扩容时会将旧空间的对象转移到新空间,如果容器不能确定该对象的移动构造函数不会抛出异常的话,就会”谨慎“地使用拷贝构造函数,这样即便过程中发生了异常也能保证不破坏原来容器中的对象(移动一个对象会改变其内容)。
正文
C++ 标准的异常
C++ 提供了一系列标准的异常,定义在 \<exception> 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
lambda
lambda
是c++11非常重要也是最常用的特性之一,他有以下优点:
- 可以就地匿名定义目标函数或函数对象,不需要额外写一个函数
lambda
是一个匿名的内联函数
lambda
表达式定义了一个匿名函数,语法如下:
[capture](params) -> ret {body;};
其中capture
是捕获列表,params
是参数列表,ret
是返回值类型,body
是函数体。
捕获列表[]
:捕获一定范围内的变量
参数列表()
: 和普通函数的参数列表一样,如果没有参数参数列表可以省略不写
返回值类型: 可以省略,编译器会自动推导
auto fun = [](){return 0;};
auto fun = []{return 0;};
捕获列表
- [] 不捕获任何变量
- [&] 捕获外部作用域中的所有变量,并且按照引用捕获
- [=]捕获外部作用域的所有变量,按照值捕获,拷贝过来的副本在函数体内是只读的
- [= ,&a] 按值捕获外部作用域中的所有变量,并且按照引用捕获外部变量 a
- [bar] 按值捕获bar变量,不捕获其他变量
- [this] 捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限
#include <iostream>
using namespace std;
class Test {
int num;
public:
void output(int x, int y) {
// auto f1 = [](){return num + x + y }; // 错误,没有捕获外部变量,不能使用类成员
auto f2 = [=] { return num + x + y; }; // 正确,以值拷贝的方式捕获所有外部变量
auto f3 = [&] { return num + x + y; }; // 正确,以引用的方式捕获所有外部变量
auto f4 = [this] { return num; }; // 正确,捕获 this 指针,可访问对象内部成员
// auto f5 = [this]{return num + x + y;}; // 错误,捕获 this 指针,可访问类内部成员,没有捕获到变量 x,y,因此不能访问。
auto f6 = [this, x, y] { return num + x + y; }; // 正确,捕获 this 指针,x,y
auto f7 = [this] { return num++; }; // 正确,捕获 this 指针,并且可以修改对象内部变量的值
}
};
int main() {
int a = 0, b = 1;
// auto f1 = []{return a}; //错误,没有捕获外部变量
auto f2 = [&]() -> int { return a + b++; }; // 正确,使用引用的方式捕获外部变量,可读写
auto f3 = [=]() -> int { return a; }; // 正确,使用值拷贝的方式捕获外部变量,可读
// auto f4 = [=] {return a++; }; // 错误,使用值拷贝的方式捕获外部变量,可读不能写
// auto f5 = [a] {return a + b; }; // // 错误,使用拷贝的方式捕获了外部变量 a,没有捕获外部变量 b,因此无法访问变量 b
auto f6 = [a, &b] { return a + (b++); }; // 正确,使用拷贝的方式捕获了外部变量 a,只读,使用引用的方式捕获外部变量 b,可读写
auto f7 = [=, &b] { return a + (b++); }; // // 正确,使用值拷贝的方式捕获所有外部变量以及 b 的引用,b 可读写,其他只读
return 0;
}
返回值
一般情况下,不指定lambda
表达式的返回值,编译器会根据return
语句自动推导返回值类型,但是需要注意的是lambda
表达式不能通过列表初始化自动推导出返回值类型。
int main() {
auto f1 = [](int a) { return a; }; // 可以自动推导出返回值类型
// auto f2 = [](){return {1,2};}; // 不能推导出返回值类型基于列表初始化推导返回值,错误
auto f2 = []() -> vector<int> {return {1,2};}; //正确显示声明了函数的返回值类型
return 0;
}
与STL搭配使用:
#include <iostream>
using namespace std;
#include<vector>
#include<algorithm>
int main() {
vector<int> vec = {1, 2, 3, 4, 5, 6};
sort(vec.begin(), vec.end(), [](int a, int b) { //自定义比较函数
return a > b;
});
for (auto it: vec) {
cout << it << " ";
}
}
#include <iostream>
using namespace std;
#include<vector>
#include<algorithm>
class Test {
public:
int num;
Test(int n) { num = n; }
};
int main() {
vector<Test> vec = {1, 2, 4, 3, 5, 6};
sort(vec.begin(), vec.end(), [](Test a, Test b) { //用于比较自定义类型
return a.num > b.num;
});
for (auto it: vec) {
cout << it.num << " ";
}
}
final & override
c++增加了final
关键字来限制某个类不能被继承或者某个虚函数不能被重写,如果final
修饰函数只能修饰虚函数,并且要把final
关键字放到类或者函数的后面。
如果使用final
修饰函数,只能修饰虚函数,这样就可以阻止子类重写父类这个函数
使用final
关键字修饰过得类不允许被继承,也就是说这个类不能有子类
#include <iostream>
using namespace std;
class Base{
public:
virtual void fun(){
cout << "base fun" << endl;
}
};
class Child : public Base{
public:
void fun() override{ //明确这是重写
cout << "Child fun" << endl;
}
virtual void final_fun() final{ //不可重写final修饰的函数
cout << "Child fun" << endl;
}
// void fun(int a) override{ //明确这是重写 但是参数不同
// cout << "base fun" << endl;
// }
};
class BrandChild final: public Child{ //如果Child类被final修饰则不可继承
public:
void fun() override{ //明确这是重写
cout << "BrandChild fun" << endl;
}
// void final_fun() { //不可重写final修饰的函数
// cout << "Child fun" << endl;
// }
// void fun(int a) override{ //明确这是重写 但是参数不同
// cout << "base fun" << endl;
// }
};
override关键字明确的表明将会重写父类的虚函数,和final的用法相同,放在函数后面。提高了程序的正确性,降低了出错概率。
智能指针
shared_ptr(共享智能指针)
初始化
共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针share_ptr
是一个模板类,如果进行初始化有三种方式如下:
- 通过构造函数初始化
std::make_shared
辅助函数reset
方法
共享智能指针对象初始化完毕之后就指向了要管理的那块堆区内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count
。
构造函数初始化
#include <iostream>
#include <memory>
using namespace std;
int main() {
//构造函数初始化
shared_ptr<int> ptr1(new int(520));
cout << "ptr1引用计数" << ptr1.use_count() << endl;
shared_ptr<char> ptr2(new char[520]); //指向一块字符数组对应的堆区内存
cout << "ptr2引用计数" << ptr2.use_count() << endl;
shared_ptr<int> ptr3;
cout << "ptr3引用计数" << ptr3.use_count() << endl;
shared_ptr<int> ptr4(nullptr);
cout << "ptr4引用计数" << ptr4.use_count() << endl;
//注意, 不可以是用同一个原始指针初始化多个不同的智能指针,会造成资源重复释放
int *p = new int (10);
shared_ptr<int> p1(p), p2(p); // 用原始指针初始化,两个智能指针引用计数都为1
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
// ptr1引用计数1
// ptr2引用计数1
// ptr3引用计数0
// ptr4引用计数0
// 1
// 1
//程序奔溃 重复释放
}
拷贝和移动构造函数初始化
当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被调用了。
#include <iostream>
#include <memory>
using namespace std;
int main() {
//构造函数
shared_ptr<int> ptr1(new int(520));
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
//拷贝构造函数
shared_ptr<int> ptr2(ptr1);
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
shared_ptr<int> ptr3 = ptr1;
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
//移动构造函数
shared_ptr<int> ptr4(move(ptr1));
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
std::shared_ptr<int> ptr5 = move(ptr2);
cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; //资源被转移了,输出0
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl; //资源被转移了,输出0
// 打印结果如下:
// ptr1管理的内存引用计数: 1
// ptr2管理的内存引用计数: 2
// ptr3管理的内存引用计数: 3
// ptr4管理的内存引用计数: 3
// ptr5管理的内存引用计数: 3
// ptr1管理的内存引用计数: 0
// ptr2管理的内存引用计数: 0
}
std::make_shared初始化
通过c++11提供的std::make_shared()就可以完成内存对象的创建并将其初始化给智能指针,make_shared()参数为<>内类型的构造函数的参数
#include <iostream>
#include <memory>
using namespace std;
class Test {
public:
Test() {
cout << "无参构造函数" << endl;
}
Test(int x) {
cout << "int类型构造函数 " << x << endl;
}
Test(string str) {
cout << "string类型的构造函数" << str << endl;
}
~Test() {
cout << "析构函数" << endl;
}
};
int main() {
shared_ptr<int> ptr1 = make_shared<int>(100);
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
shared_ptr<Test> ptr2 = make_shared<Test>(); //括号内传的是Test 的构造函数参数
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
shared_ptr<Test> ptr3 = make_shared<Test>(520);
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
shared_ptr<Test> ptr4 = make_shared<Test>("aaaa");
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
shared_ptr<Test> ptr5(new Test(200));
cout << "ptr5的引用计数为:" << ptr5.use_count() << endl;
return 0;
}
//ptr1管理的内存引用计数: 1
//无参构造函数
//ptr2管理的内存引用计数: 1
//int类型构造函数 520
//ptr3管理的内存引用计数: 1
//string类型的构造函数aaaa
//ptr4管理的内存引用计数: 1
//int类型构造函数 200
//ptr5的引用计数为:1
//析构函数
//析构函数
//析构函数
//析构函数
如果使用拷贝的方式初始化共享智能指针,这两个对象会同时管理同一块内存,堆内存对应的引用计数也会增加。如果使用移动构造的方式初始化智能指针对象,只是转让了内存的所有权,管理内存的对象不会增加,因此内存引用技术不会增加。
reset
方法初始化
#include <iostream>
#include <string>
#include <memory>
using namespace std;
int main()
{
// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
shared_ptr<int> ptr1 = make_shared<int>(520);
shared_ptr<int> ptr2 = ptr1;
shared_ptr<int> ptr3 = ptr1;
shared_ptr<int> ptr4 = ptr1;
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
ptr4.reset(); //重置为空
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
shared_ptr<int> ptr5;
ptr5.reset(new int(250)); // //重置为new int(250)
cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;
return 0;
}
//打印结果如下:
//ptr1管理的内存引用计数: 4
//ptr2管理的内存引用计数: 4
//ptr3管理的内存引用计数: 4
//ptr4管理的内存引用计数: 4
//
//ptr1管理的内存引用计数: 3
//ptr2管理的内存引用计数: 3
//ptr3管理的内存引用计数: 3
//ptr4管理的内存引用计数: 0
//
//ptr5管理的内存引用计数: 1
对于一个未初始化的共享智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1.
获取原始指针
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> ptr (new int(1));
cout << *(ptr.get()) /*对原始地址解引用*/ << " " << *ptr /*重载 * 运算符*/ << endl;
//1 1
}
unique_ptr(独占智能指针)
std::unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
// 通过构造函数初始化对象
unique_ptr<int> ptr1(new int(100)); //独占智能指针
// 报错
// unique_ptr<int> ptr2 = ptr1; // 拷贝构造 = delete
unique_ptr<int> ptr2;
// ptr2 = ptr1; // 重载赋值运算符 = delete
}
#include <iostream>
#include <memory>
using namespace std;
unique_ptr<int> fun(){
unique_ptr<int> ptr1(new int(100));
return ptr1;
}
int main()
{
unique_ptr<int> p = fun(); //这里走的是移动构造所以不报错
}
unique_ptr不允许被复制,但是可以通过函数返回给其他的unique_ptr,还可以通过std::move()转移给其他的unique_ptr。还是一个unique_ptr独占一个地址。
使用 reset 方法可以让 unique_ptr 解除对原始内存的管理,也可以用来初始化一个独占的智能指针。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> ptr1(new int(10));
unique_ptr<int> ptr2;
ptr1.reset(); //解除对原始内存的管理
ptr2.reset(new int(250)); //重新指定智能指针管理的原始内存
}
如果想要获取独占智能指针管理的原始地址,可以调用 get () 方法
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> ptr1(new int(10));
unique_ptr<int> ptr2 = move(ptr1);
ptr2.reset(new int(250));
cout << *(ptr2.get()) << endl; // 得到内存地址中存储的实际数值 250
return 0;
}
weak_ptr(弱引用智能指针)
弱引用智能指针 std::weak_ptr 可以看做是 shared_ptr 的助手,它不管理 shared_ptr 内部的指针。std::weak_ptr 没有重载操作符 * 和->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视 shared_ptr 中管理的资源是否存在。可以解决shared_ptr 循环引用导致内存泄漏的问题。
#include <iostream>
#include <memory>
using namespace std;
int main(){
shared_ptr<int> sp (new int(100));
weak_ptr<int> wp1;// 构造了一个空 weak_ptr 对象
weak_ptr<int> wp2(wp1); // 通过一个空 weak_ptr 对象构造了另一个空 weak_ptr 对象
weak_ptr<int> wp3(sp); // 通过一个 shared_ptr 对象构造了一个可用的 weak_ptr 实例对象
weak_ptr<int> wp4;
wp4 = sp; // 通过一个 shared_ptr 对象构造了一个可用的 weak_ptr 实例对象(这是一个隐式类型转换)
weak_ptr<int> wp5;
wp5 = wp3; // 通过一个 weak_ptr 对象构造了一个可用的 weak_ptr 实例对象
return 0;
}
use_count()
通过调用 std::weak_ptr 类提供的 use_count() 方法可以获得当前所观测资源的引用计数
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp(new int);
weak_ptr<int> wp1;
weak_ptr<int> wp2(wp1);
weak_ptr<int> wp3(sp);
weak_ptr<int> wp4;
wp4 = sp;
weak_ptr<int> wp5;
wp5 = wp3;
cout << "use_count: " << endl;
cout << "wp1: " << wp1.use_count() << endl; // 0
cout << "wp2: " << wp2.use_count() << endl; // 0
cout << "wp3: " << wp3.use_count() << endl; // 1
cout << "wp4: " << wp4.use_count() << endl; // 1
cout << "wp5: " << wp5.use_count() << endl; // 1
return 0;
}
通过打印的结果可以知道,虽然弱引用智能指针 wp3、wp4、wp5 监测的资源是同一个,但是它的引用计数并没有发生任何的变化,也进一步证明了 weak_ptr只是监测资源,并不管理资源。
expired()
通过调用 std::weak_ptr 类提供的 expired() 方法来判断观测的资源是否已经被释放
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.expired() << endl; // 0 未被释放
sp.reset();
cout << wp.expired() << endl; // 1 被释放
return 0;
}
weak_ptr 监测的就是 shared_ptr 管理的资源,当共享智能指针调用 shared.reset(); 之后管理的资源被释放,因此 weak.expired() 函数的结果返回 true,表示监测的资源已经不存在了。
lock()
通过调用 std::weak_ptr 类提供的 lock() 方法来获取管理所监测资源的 shared_ptr 对象
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp1, sp2;
weak_ptr<int> wp;
sp1 = make_shared<int>(100);
wp = sp1;
sp2 = wp.lock();
cout << "use_count: " << wp.use_count() << endl; // 2 sp1 和 sp2
sp1.reset();
cout << "use_count: " << wp.use_count() << endl; // 1 sp2
sp1 = wp.lock();
cout << "use_count: " << wp.use_count() << endl; // 2 sp1 和 sp2
cout << "*sp1: " << *sp1 << endl; //100
cout << "*sp2: " << *sp2 << endl; //100
return 0;
}
sp2 = wp.lock(); 通过调用 lock() 方法得到一个用于管理 weak_ptr 对象所监测的资源的共享智能指针对象,使用这个对象初始化 sp2,此时所监测资源的引用计数加一.
sp1.reset(); 共享智能指针 sp1 被重置,weak_ptr 对象所监测的资源的引用计数减 1.
sp1 = wp.lock(); sp1 重新被初始化,并且管理的还是 weak_ptr 对象所监测的资源,因此引用计数加 1.
共享智能指针对象 sp1 和 sp2 管理的是同一块内存,因此最终打印的内存中的结果是相同的,都是 520.
reset()
通过调用 std::weak_ptr 类提供的 reset() 方法来清空对象,使其不监测任何资源。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp1(new int(100));
weak_ptr<int> wp(sp1);
cout << wp.expired() << " " << wp.use_count() << endl; // 0 1
wp.reset();
cout << wp.expired() << " " << wp.use_count() << endl;// 1 0
return 0;
}
返回管理this的 share_ptr
#include <iostream>
#include <memory>
using namespace std;
struct Test {
shared_ptr<Test> getSharedPtr() {
return shared_ptr<Test>(this); // this 等价于main函数中的 new Test, 这里相当于使用同一个原始指针创建了多个智能指针
}
~Test() {
cout << "析构函数" << endl;
}
};
int main() {
shared_ptr<Test> sp1(new Test);
cout << "引用个数 " << sp1.use_count() << endl;
shared_ptr<Test> sp2 = sp1->getSharedPtr();
cout << "引用个数: " << sp1.use_count() << endl;
return 0;
}
//
//引用个数 1
//引用个数: 1
//析构函数
//析构函数
// 程序奔溃,同一块堆区内存重复释放
通过输出的结果可以看到一个对象被析构了两次,其原因是这样的:在这个例子中使用同一个指针 this 构造了两个智能指针对象 sp1 和 sp2,这二者之间是没有任何关系的,因为 sp2 并不是通过 sp1 初始化得到的实例对象。在离开作用域之后 this 将被构造的两个智能指针各自析构,导致重复析构的错误。
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A
{
public:
shared_ptr<B> bptr;
~A()
{
cout << "class TA is destruct ..." << endl;
}
};
class B
{
public:
shared_ptr<A> aptr;
~B()
{
cout << "class TB is destruct ..." << endl;
}
};
void testPtr()
{
shared_ptr<A> ap(new A);
shared_ptr<B> bp(new B);
cout << "A 的 引用计数: " << ap.use_count() << endl;
cout << "B 的 引用计数: " << bp.use_count() << endl;
ap->bptr = bp;
bp->aptr = ap;
cout << "A 的 引用计数: " << ap.use_count() << endl;
cout << "B 的 引用计数: " << bp.use_count() << endl;
}
int main()
{
testPtr();
return 0;
}
//A 的 引用计数: 1
//B 的 引用计数: 1
//A 的 引用计数: 2
//B 的 引用计数: 2
// A 和 B 的对象没有被析构, 因为 在testPtr函数作用域结束时 ap, bp 的引用计数减为一,所以不会销毁
共享智能指针 ap、bp 对 A、B 实例对象的引用计数变为 2,在共享智能指针离开作用域之后引用计数只能减为1,这种情况下不会去删除智能指针管理的内存,导致类 A、B 的实例对象不能被析构,最终造成内存泄露。通过使用 weak_ptr 可以解决这个问题,只需要将类 A 或者 B 的任意一个成员改为 weak_ptr.。
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A
{
public:
shared_ptr<B> bptr; //或者将它改为weak_ptr
~A()
{
cout << "class TA is disstruct ..." << endl;
}
};
class B
{
public:
weak_ptr<A> aptr;
~B()
{
cout << "class TB is disstruct ..." << endl;
}
};
void testPtr()
{
shared_ptr<A> ap(new A);
shared_ptr<B> bp(new B);
cout << "A 的 引用计数: " << ap.use_count() << endl;
cout << "B 的 引用计数: " << bp.use_count() << endl;
ap->bptr = bp;
bp->aptr = ap;
cout << "A 的 引用计数: " << ap.use_count() << endl;
cout << "B 的 引用计数: " << bp.use_count() << endl;
}
int main()
{
testPtr();
return 0;
}
//A 的 引用计数: 1
//B 的 引用计数: 1
//A 的 引用计数: 2
//B 的 引用计数: 2
// A 和 B 的对象没有被析构, 因为 在testPtr函数作用域结束时 ap, bp 的引用计数减为一,所以不会销毁
上面程序中,在对类 A 成员赋值时 ap->bptr = bp; 由于 bptr 是 weak_ptr 类型,这个赋值操作并不会增加引用计数,所以 bp 的引用计数仍然为 1,在离开作用域之后 bp 的引用计数减为 0,类 B 的实例对象被析构。
在类 B 的实例对象被析构的时候,内部的 aptr 也被析构,其对 A 对象的管理解除,内存的引用计数减为 1,当共享智能指针 ap 离开作用域之后,对 A 对象的管理也解除了,内存的引用计数减为 0,类 A 的实例对象被析构。
shared_ptr的实现
shared_ptr 通过让具有相同指向的对象,共同维护同一块堆区控制块对象进行计数。使用同一个原始指针构造多个shared_ptr,每个shared_ptr对象都会新建一个控制块对象,从而导致资源重复释放。
#include <iostream>
#include <memory>
int main() {
auto p = std::make_shared<int>(4);
auto p1 = std::shared_ptr<int>(new int(4));
std::cout << *p << *p1 << "\n";
return 0;
}
上述对象p的内存布局如下所示
深入理解Modern C++智能指针std::shared_ptr - 知乎 (zhihu.com)
以下是MyShared_ptr 的简单实现
//
// Created by 于 on 2024/2/27.
//
#pragma once
#include <iostream>
template<typename T>
class Ref {
T *obj = nullptr;
int count = 0;
public:
Ref(T *const newP) : obj(newP) { //构造函数, 设置obj指针并增加引用计数
count += 1;
}
void reduce() { //减少引用计数
count -= 1;
if (count == 0) { //引用计数为 0 时进行清理
delete obj;
delete this;
}
}
inline void increase() { //增加引用计数
count += 1;
}
inline int get_count() {
return count;
}
inline T *get_obj() {
return this->obj;
}
};
/*
无参构造 传递指针构造 拷贝构造 移动构造 赋值运算符
reset()
operator* ,operator->
get()
use_count()
*/
template<typename T>
class MyShared_ptr {
Ref<T> *r = nullptr;
public:
MyShared_ptr() = default; //无参构造
explicit MyShared_ptr(T *const newP) { //传递指针构造
std::cout << "----------------调用构造函数--------------------" << std::endl;
this->r = new Ref<T>(newP);
}
MyShared_ptr(const MyShared_ptr &other) {
std::cout << "----------------调用拷贝构造函数--------------------" << std::endl;
r = other.r;
r->increase();
}
MyShared_ptr(MyShared_ptr &&other) {
std::cout << "----------------调用移动构造函数--------------------" << std::endl;
r = other.r;
other.r = nullptr;
}
int use_count() {
if (r) return r->get_count();
return 0;
}
/*
赋值运算符是,一个已经存在的对象给另一个已经存在的对象赋值
所以首先判断this->r 是否指向一块内存,如果指向讲该内存的引用计数减一
然后修改this->r = other.r
然后讲this->r 的 引用计数加1
*/
MyShared_ptr &operator=(const MyShared_ptr &other) {
std::cout << "----------------调用拷贝赋值--------------------" << std::endl;
if (this == &other) return *this; // 判断自赋值
if (r) r->reduce();
r = other.r;
if (r) r->increase();
return *this;
}
MyShared_ptr &operator=(MyShared_ptr &&other) {
std::cout << "----------------调用移动赋值--------------------" << std::endl;
if (r) r->reduce();
r = other.r;
other.r = nullptr;
return *this;
}
void reset() {
if (r) r->reduce();
r = nullptr;
}
void reset(T *newP) {
if (r) r->reduce();
r = new Ref<T>(newP);
}
T &operator*() {
if (r) return *(r->get_obj());
}
T *operator->() {
if (r) return r->get_obj();
return nullptr;
}
T *get() {
if (r) return r->get_obj();
return nullptr;
}
~MyShared_ptr() {
if (r) r->reduce();
}
};
#include <iostream>
#include "MyShared_ptr.h"
using namespace std;
struct Test{
int a = 1;
~Test(){
cout << "Test 析构" << endl;
}
};
int main()
{
MyShared_ptr<int> p0;
MyShared_ptr<int> p(new int(100));
cout << p.use_count() << endl;
MyShared_ptr<int> p1(p);
cout << p1.use_count() << endl;
MyShared_ptr<int> p2(std::move(p1));
cout << p2.use_count() << endl;
MyShared_ptr<int> p3;
p3 = p0;
cout << p3.use_count() << endl;
p3 = p2;
cout << p3.use_count() << endl;
p3 = move(p2);
cout << p3.use_count() << endl;
p3.reset();
cout << p3.use_count() << endl;
p3.reset(new int(2));
cout << p3.use_count() << endl;
cout << *p3 << endl;
cout << *p3.get() << endl;
MyShared_ptr<Test> p4(new Test());
cout << p4->a << endl;
MyShared_ptr<Test> p5(p4);
cout << p5.use_count() << endl;
MyShared_ptr<int> p6;
// cout << *p6 << endl; //崩溃
return 0;
}
//输出结果
// ----------------调用构造函数--------------------
// 1
// ----------------调用拷贝构造函数--------------------
// 2
// ----------------调用移动构造函数--------------------
// 2
// ----------------调用拷贝赋值--------------------
// 0
// ----------------调用拷贝赋值--------------------
// 3
// ----------------调用移动赋值--------------------
// 2
// 0
// 1
// 2
// 2
// ----------------调用构造函数--------------------
// 1
// ----------------调用拷贝构造函数--------------------
// 2
// Test 析构
程序分段
bss段(bss segment): bss是Block Started by Symbol的简称,用来存放程序中未初始化或初始化为0的全局变量的内存区域,属于静态内存分配。
data段(data segment): 用来存放程序中已初始化的全局变量的内存区域,属于静态内存分配。
text段(text segment): 用来存放程序执行代码的内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。也有可能包含一些只读的常数变量,例如字符串常量等。
堆(heap): 用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
栈(stack): 用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在data段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。
新特性
为什么需要将移动构造函数和移动赋值运算符标记为 noexcept - Blog of Flash (young-flash.github.io)
c++11新特性,所有知识点都在这了!-腾讯云开发者社区-腾讯云 (tencent.com)
【翻译】C++14的新特性简介-腾讯云开发者社区-腾讯云 (tencent.com)
c++11
关键字:
- auto
- decltype
- nullptr
- final
- override
- constexpr
语法:
- 基于范围的for循环
- function 函数对象 : lambda 、bind
右值引用:
- 将亡值
- 移动构造
- move
- 模板万能引用 T&&
- forward
STL:
- array
- forward_list
- unordered_map
- unordered_set
智能指针:
- shared_ptr、weak_ptr
- unique_ptr
多线程:
- thread
- mutex、 lock_guard
- condithon_variable
- atomic
四种类型转换
C++引入了四种更加安全的强制类型转换运算符,分别是const_cast
、static_cast
、reinterpret_cast
和dynamic_cast
,作用和区别如下:
const_cast:
去除const(volatile)属性, 只针对指针、引用和this指针有效,其他情况会报错
示例
int main() {
const int a = 0;
// int *p = &a; // 这样会报错
int *p = const_cast<int*>(&a); // 去除了 const 属性
// int c = const_cast<int>(a); // 报错,因为只针对指针、引用和this指针有效
return 0;
}
static_cast:
用于各种隐式转换例如
void*
转换为任意类型的指针- 任意类型的指针转换为
void*
- 编译器允许的跨类型转换,比如
char
类型转换为int
类型,double
转int
型
static_cast
不做类型检查, 允许子类类型的指针安全转换为父类类型的指针,相反也能转换能成功但是不安全,结果未知;只能在拥有继承关系的类之间进行转换示例
class Base { }; class Derived : public Base { }; int main() { Derived* d = new Derived(); Base* b = static_cast<Base*>(d); // 子类转换为父类, 是安全的 Base* b1 = new Base(); Derived* d1 = static_cast<Derived*>(b1); // 父类转换为子类,不安全 return 0; }
dynamic_cast:
动态类型转换,只能用于含有虚函数的类, 不然会报错,
dynamic_cast
会做类型检查, 检测在运行时进行。如果是非法的,对于指针返回NULL,对于引用抛bad_cast异常。将子类类型的指针转换为父类类型的指针与static_cast
是一样的示例
class Base { virtual void dummy() {} // 如果没有虚函数dynamic_cast会编译报错}; class Derived : public Base {}; int main() { //指针 Base* b1 = new Derived; Base* b2 = new Base; Derived* d1 = dynamic_cast<Derived *>(b1); // 成功 Derived* d2 = dynamic_cast<Derived *>(b2); // 失败返回空指针 //引用 Base& b3 = *b1; Base& b4 = *b2; try { Derived& d3 = dynamic_cast<Derived&>(b3); // 成功 Derived& d4 = dynamic_cast<Derived&>(b4); // 失败抛出异常 } catch (bad_cast& e) { cout << e.what() << endl; } }
reinterpret_cast
就像传统的强制类型转换一样对待所有指针的类型转换。几乎什么都可以转,比如将int转指针,可能会出问题, 它不进行检查,只是进行强制的复制
示例
int main() { int a = 10; int* p = &a; int* c = reinterpret_cast<int*>(a); // 可以把整数转成指针 char* p1 = reinterpret_cast<char*>(p); // 可以把指针转成另一种指针 }
相比于 c 的强制类型转换的好处
C 的强制转换表面上看起来功能强大什么都能转,但是转化语义不够明确,不能进行错误检查,容易出错。
委托构造
委托构造函数是C++11引入的一个特性,它允许一个构造函数调用同一类的另一个构造函数,从而避免在类内部出现相似的初始化代码,提高代码的可维护性。在构造函数的初始化列表中使用 : 符号,可以调用同一类中的其他构造函数。
注意事项
- 不要形成委托环。
- 如果在委托构造函数中使用 try,可以捕获目标构造函数中抛出的异常。
示例
class Test {
public:
Test() {
cout << "Test( )" << endl;
}
Test(int a) : Test(){ // 委托使用无参的构造
cout << "Test(int a)" << endl;
}
void fun() {
cout << "fun" << endl;
}
};
如果只是在函数体里调用被委托的构造函数,则只会执行被委托构造函数函数体里的内容(忽略初始化操作),此时委托构造函数的初始化列表负责对象成员的初始化,如果对象成员在初始化列表中被忽略,则执行默认初始化(当然,这时它也只是一个普通构造函数,而非委托构造函数)。
委托构造函数的初始值列表中,只允许出现被委托的构造函数,而不能直接给成员变量进行初始化。
先执行被委托构造函数的初始化列表,然后执行被委托构造函数的函数体,最后返回执行委托构造函数的函数体。
被委托的构造函数同样可以是一个委托构造函数,它继续委托另一个构造函数完成初始化任务。
继承构造
C++11中提供的继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。通过使用using 类名::构造函数名(其实类名和构造函数名是一样的)来声明使用基类的构造函数, 另外如果在子类中隐藏了父类中的同名函数,也可以通过using的方式在子类中使用基类中的这些父类函数:
示例
class Base {
int a;
public:
Base() {
cout << "Base(int a)" << endl;
this->a = 0;
};
};
class Derived : public Base {
public:
using Base::Base;
void fun() {
cout << "fun" << endl;
}
};
其他
对象切片
如果把一个派生类对象直接赋值给基类对象,会发生对象切片(object slicing)的现象。对象切片是指当子类对象赋值给基类对象时,会丢失子类对象中的特有成员,只保留基类部分的成员。
弹性数组