【C++】第三篇:面向对象(类和对象,内存管理)

在这里插入图片描述

从面向过程到面向对象之类的引入

阅读完这个版块你可以了解以下内容

1)面向过程和面向对象的区别

2)类的定义和使用

3)类的大小计算

4)this指针的存放位置和this是否可以为空

对面向过程和面向对象的认识

面向过程注重的是过程,也就是分析问题的步骤,靠的是变量和函数调用,其中变量和函数是分离开来的

面向对象注重的是对象,将一件事情分成了不同的对象,靠的是对象之间的交互,其中对象中结合了数据成员和函数(有点像离散数学中的代数系统

**面向过程的语言最经典的就是C语言,面向对象的有很多高级语言像C++,Java,go….**简单的来说对象就是将面向过程中的变量和函数整合到一个体系当中,那就是对象。另外面向对象有四大特点:抽象,封装,继承,多态。而对象的抽象就是类,类也可以实例化(定义)出对象,所以就要先谈谈类的定义。

类的引入

在C++中,定义类可以用两个关键字classstruct,这两个都可以定义一个类,但有所区别(在下文我会汇总问题统一回答。)但是一般习惯上都是用class定义一个类

类的定义

class className
{
   // 类体:由成员函数和成员变量组成
 
}; // 一定要注意后面的分号

class定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号

定义类的两种方式:

1)声明和定义都放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数

理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hQNuF0Be-1622384260183)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528163643471.png)]

2) 声明放在.h的头文件中, 定义放在.cpp文件中,分文件编写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0NLm6L4a-1622384260184)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528163856398.png)]

类的访问限定符

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AQiWTim8-1622384260186)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528164339172.png)]

【访问限定符说明】

  1. public修饰的成员在类外可以直接被访问

  2. protected和private修饰的成员在类外不能直接被访问(但在类中可以被访问)

  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

  4. class的默认访问权限为private

    class A
    {
    	void fun4();// 默认私有,类外不可调用
    
    public:
    	void fun1();// public限定,类外可以调用
    
    protected:
    	void fun2();// protected限定,类外不可调用
    
    private:
    	void fun3();// private限定,类外不可调用
    
    };
    

了解了访问限定符就可以回答上面遗留的问题了

class和struct定义类有什么区别?

答:唯一的区别就是默认的访问权限不同,class默认访问权限是public,struct的默认访问权限是private.

注意:为了保护成员数据和给用户使用接口,所以一般把数据成员写在private,给外界调用的成员函数写在public

类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中**。**在类体外定义成员,需要使用::作用域解析符

指明成员属于哪个类域。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mRWXPyxy-1622384260187)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528165509299.png)]

刚刚在说分文件编写的时候,类中的成员函数在类内声明,在类外定义,但是叫show的函数可能有很多,怎么知道这个函数是Date类的呢?就是通过Date::类的作用域解析符来判别的。

封装的意义

类是对象的抽象,一个类其实也把数据和函数都封装起来的,封装有以下的几点好处

1)可以隐藏内部的细节,对外提供公共的访问接口。

2)提高安全性,防止数据成员被修改

类的实例化

类的大体框架了解过后,就可以回到对象上了,前文也讲过其实类就是对象的抽象,对象就是类的实例化

class Date
{
public:
	// 打印日期(inline函数)
	void show();
private:
	int _year; // 年
	int _month;// 月
	int _day;  // 日
};

类是一个抽象化的东西,就像上面这个类一样,它只说明了可以定义一个日期,但是日期还是得具体问题具体分析,所以你可以定义一个今天的日期xxxx年xx月xx日的日期,还可以定义一个明天的日期xxxx年xx月xx日,这些被具体化出来的日期就是一个个对象。

类大小的计算

class A
{
public:
	void fun1();
    void fun2();
private:
    int a;
    char b;
};
对象的存储方式

在一个类中有两部分组成,一部分是数据成员,一部分是成员函数,这两个部分都是在类中声明的,所以他们的存储方式就决定了一个类的大小。但是无非就只有两种存储方式。

1)数据成员和成员函数都存放在类中,每一实例化出的对象中都包含这两个部分。

但是可以想一想,每个对象的数据成员肯定是以对象而异的,但是所用的对象调用的函数不都是一样的嘛。所有就出现了第二种存储方式,也就是真真的存储方式

2)对象只保存成员变量,成员函数保留在公共的代码段中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wmHlb5O7-1622384260189)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528172045933.png)]

总结:计算类的大小的时候可以只算成员变量的大小之和,成员函数就可以不用管了

思考:要是类中没有数据成员变量,那类的大小是多少呢?

class A
{
    // 空..
};

有人认为是0字节,但是这个类已经被定义出来了,所以不可能是0字节,答案是这个类比较特殊所以编译器给了空类一个字节来唯一标识这个类.

sizeof(A) == 0

计算类的大小要注意的几点:

1)成员函数不在计算的范围之内

2)空类的大小为1

3)在注意到上面两种情况小,计算普通的一个类的大小和计算结构体一样,需要满足内存对齐规则)

this指针

上文已经了解过了,成员函数是放在公共代码段中的,而数据成员是每个对象都独有的,但是有一个问题,当对象调用成员函数的时候,函数是怎么识别每一个对象的呢?

int main()
{
    
	Date d1;
	Date d2;
	d1.show();// d1是怎么调用show()函数的?
	d2.show();// d2是怎么调用show()函数的?

	return 0;
}

成员函数是通过一个名为this的指针的额外的隐式参数来访问调用的那个对象。

this指针的特点:

  1. this指针的类型:类的类型* const(例如日期类中this就是Date* const 类型)

  2. 只能在“成员函数”的内部使用

  3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this

形参。所以对象中不存储this指针

  1. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递
int main()
{
    
	Date d1;
	Date d2;
	d1.show();// 相当于d1.show(&d1);
	d2.show();// 相当于d2.show(&d2);
	// 但是this在实际中,是不能显示的写出来的
	return 0;
}
inline void Date::show(Date* this)
{
    cout << _year << "-" << _month << "-" << _day << endl;
}

注意

1)this指针就是隐式的指针,在调用的时候不可以显式的写出来。

2)this存放的地方根据编译器不同可能会不同,可能是存放在栈上,但是VS上this存放在寄存器中传递

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jRM49Hga-1622384260189)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528180502678.png)]

最后通过一道题目来更深刻的了解this指针

class Date
{
public:
	void PrintDate()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	void Show()
	{
		cout << "日期显示" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date* p = nullptr;
    // // 可以调用以下这两个函数吗?
	p->PrintDate();
	p->Show();
}

请思考过后在看答案

答案:

p->PrintDate();不可以调用

p->Show();可以调用

有些同学肯定觉得两个函数都不能调用,因为p是空指针,所以用->解引用就会报错,但是真的是p指针在调用吗?p指针就是指向Date类的一个指针,但是他指向的对象有点特殊是一个空,所以这个对象的地址就是nullptr,相当于thisnullptr。在成员函数调用的时候是将对象的地址传给函数了,函数在接收地址后,执行对象想要对数据的操作,在调用p->PrintDate()的时候,函数进行了访问_year_这个成员数据,再写的清楚一点就是this->_year,但是this是nullptr,这时就是空指针访问了,所以程序就崩溃了。在调用p->Show(),尽管this还是nullptr,但是函数的内部就是打印了一句话而已,并没有对this进行任何的操作,所以程序就不会崩溃。

回答问题:

  1. this指针存在哪里?

  2. this指针可以为空吗?

1.this指针存在哪里?

其实编译器在生成程序时加入了获取对象首地址的相关代码。并把获取的首地址存放在了寄存器ECX中也就是成员函数的其它参数正常都是存放在栈中。而this指针参数则是存放在寄存器中。

2.this指针可以为空吗?

可以为空,当我们在调用函数的时候,如果函数内部并不需要使用到this,也就是不需要通过this指向当前对象并对其进行操作时才可以为空,如果调用的函数需要指向当前对象,并进行操作,则会发生错误(空指针引用)。

从面向过程到面向对象之熟悉类的组成

类中的6个默认构造函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ucBflS6y-1622384260190)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528201242323.png)]

在一个类中(即使是空类)会有自动生成6个默认的默认构造函数。

默认构造成员函数:在我们不在类中自己主动声明和定义的时候,类中就会自动生成的函数

默认构造函数的意义:在C语言中,如果我们要写一个Date的结构体或者Stack这样的数据结构,是不是一定会写初始化结构的函数,销毁结构的函数……既然都要写,所以在C++中就默认类中必须要有初始化对象的函数,销毁对象的函数还有可以复制对象的函数,可以复制给另一个对象的函数,这分别对应着构造函数,析构函数,拷贝构造函数,拷贝赋值运算符重载。

所以这四个函数是非常重要的4个默认成员函数,就下来会一个一个介绍每一个函数。

因为这四个函数对于带指针的对象和不带指针的对象是不一样的(后面会解释),所以这四个函数会通过演示Complex类和String类的方式来解释这四个函数。

构造函数

既然每个对象在类的实例化中都要初始化,所以构造函数就应运而生了,它就是专门给对象初始化的函数,注意不要被构造函数这个名字骗了,其实并不是给对象分配空间构造出对象的空间,而是给对象中的数据初始化。

其特征如下:

  1. 函数名与类名相同。

  2. 无返回值。

  3. 对象实例化时编译器自动调用对应的构造函数。

  4. 构造函数可以重载。

Complex类(不带指针的类):

class Complex
{
public:
    // 函数名和类名相同
    // 无返回值
	Complex()
	{
		_real = 0;
		_image = 0;
	}
	// 函数重载
	Complex(int real, int image)
	{
		_real = real;
		_image = image;
	}
private:
	int _real; // 实部
	int _image;// 虚部
};

int main()
{
	// 调用无参数的构造函数
	Complex c1;
	// 调用有参数的构造函数
	Complex c2(1, 2);
	// 不会返回一个对象,编译器会把它当做一个函数的声明
	// 以Complex对象为返回值的一个叫c3的函数的函数声明
	Complex c3();
	return 0;
}

要特别注意一下上面的第三个栗子Complex c3()这样是不会创建对象的。

要是我们不写构造函数其实也是可以创建对象的

class Complex
{
public:
    /*
    // 如果不写,编译器就会调用自己默认生成的那个构造函数
	Complex(int real, int image)
	{
		_real = real;
		_image = image;
	}
	*/
private:
	int _real; // 实部
	int _image;// 虚部
};

int main ()
{
    Complex c;
    return 0;
}

如果不写,编译器就会调用自己默认生成的那个构造函数,但是会有一些不愉快的事情发生

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4k2O0IdJ-1622384260190)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528205549008.png)]

我们发现_real 和 _image并没有初始化,还是随机值,是的,编译器是不会帮你初始化的,只是不会报错而已,那构造函数还有神马用呀,其实还是有用的向下面这个例子

class A
{
public:
	A()
	{
		_a = 10;
	}
private:
	int _a;
};


class Complex
{
public:
    /*
    没写构造函数
    */
private:
	A t;
};

int main()
{
	Complex c;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ufdgxnh-1622384260190)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528210205219.png)]

a竟然初始化了,但是Complex也没有写构造函数。这就是语法规则,在类中如果数据成员是内置类型(基本数据类型)就默认不处理,但是对于自定义类型(类,结构体,联合体)的成员就调用对象的构造函数。

总结:如果类中含有内置类型的数据成员就必须要自己显式的写构造函数,这样才能保证数据初始化,如果类中只含有自定义类型的数据成员就可写可不写构造函数因为系统都会调用自定义的构造函数。(只要有一个内置类型的数据成员就必须写构造函数)

写构造函数的技巧

其实写构造函数搭配缺省函数)是最棒的,因为缺省函数可以相当与好多函数重载,这样就可以一劳永逸了

class Complex
{
public:
	Complex(int real = 0, int image = 0)
	{
		_real = real;
		_image = image;
	}
private:
	int _real; // 实部
	int _image;// 虚部
};

int main()
{
    Complex c1;// 0, 0
    Complex c2(1);// 1, 0
    Complex c3(1, 2);// 1, 2
    return 0;
}

这样就可以方便很多了

但是还有两点要注意的是

1)如果构造函数在类内声明,在类外实现,默认的参数就必须只能写在声明中,在实现的函数中就不能再写默认的参数了

class Complex
{
public:
	Complex(int real = 0, int image = 0);
private:
	int _real; // 实部
	int _image;// 虚部
};

Complex::Complex(int real = 0, int image = 0)// 错误
{
	_real = real;
	_image = image;
}

Complex::Complex(int real, int image)// 正确
{
	_real = real;
	_image = image;
}

2)默认的构造函数只能有一个,缺省的函数是默认的构造函数,不带参数的函数也是默认的构造函数,所以这两者不能同时存在,否则函数不知道要调用哪个函数就会报错

class Complex
{
public:
	Complex()// 不用传参数
	{
		_real = 0;
		_image = 0;
	}

	Complex(int real = 0, int image = 0)// 不用传参数
	{
		_real = real;
		_image = image;
	}
private:
	int _real; // 实部
	int _image;// 虚部
};


int main ()
{
    Complex c;// 调用哪一个函数呢?
    return 0;
}

初始化列表

在C++语法中初始化成员数据的方法除了上面在构造函数体内初始化之外还有一种方法叫做:初始化列表初始化

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括

号中的初始值或表达式。

class Complex
{
public:
    //                      冒号  成员数据(形参),成员数据(形参) ... {}
	Complex(int real, int image):_real(real), _image(image) {}
private:
	int _real; // 实部
	int _image;// 虚部
};


int main ()
{
    Complex c;// 调用哪一个函数呢?
    return 0;
}

在这里要解决两个问题:

1)初始化列表初始化和构造函数体内初始化的区别?

2)初始化列表的使用注意细节和好处?

1.第一个问题两个中初始化方式的不同点。其实在严格意义上讲只有在初始化列表中初始化才叫做初始化,而函数体内的初始化叫做成员数据的赋值。举个栗子:

// 方法1
int i = 0;
// 方法2
int i;
i = 0;

在上面这个栗子中,两中方法正好对应着两种初始化方式。方法一就对应这初始化列表初始化,而方法二对应着函数体内初始化。也就是说在进入构造函数之前就会进入初始化列表,如果在初始化列表中初始化,就相当于在定义的时候初始化(方法1),一旦进入了函数体内部,就意味着已经对变量声明,在函数体内者在进行赋值操作。 再简单来说就是函数体内对变量先声明再赋值,初始化列表中对变量在定义的时候初始化。

2.第二个就要谈谈初始化列表的使用细节。

1)在初始化列表中只能对一个成员变量初始化一次。

class Complex
{
public:
	Complex(int real, int image):_real(real), _image(image),_real(1) {}
private:
	int _real; // 实部
	int _image;// 虚部
};

像这样对_real初始化了两次就不可以。

2)有些数据成员可用可不用初始化列表初始化的,但是有些数据成员是必须要用初始化列表初始化的。

如果数据成员是内置类型像Complex的两个数据成员都是int类型的,就可以不用初始化列表初始化。因为初始化列表的作用就是让变量在定义的时候初始化,但是C++中内置类型的变量在声明的时候虽然不会分配空间但是会给变量一个随机值(或者默认值),如Complex中如果不给int一个初值,它自己就会有一个随机值,函数体内再赋值给变量。

int i;
i = 0;

内置类型就像这样初始化了,但是有一些必须要在定义的时候初始化的成员就一定要用初始化列表初始化。

1)const成员变量

2)引用成员变量

3)自定义类型成员并且没有默认的构造函数

const int& a;// 错误
int& b;// 错误

int t = 10;
const int& a = t;// 正确
int& b = t;// 正确

const成员只有在初始化的时候才可以给变量数值,引用不能空引用。所以const成员变量和引用成员因为必须在初始化的时候定义所以必须要在初始化列表中初始化。

如果自定义类型的成员数据有默认构造函数(1.无参构造函数2.全缺省构造函数3.系统默认生成的构造函数)可以不用初始化列表。

class A
{
public:
    // 全缺省的构造函数
    A(): _a(0) {}
private:
    int _a;
}

class B
{
public:
    B(int b) : _b(b) {}
private:
    int _b;
    A _a;
}

这样是可以的,因为在声明的时候A _a就默认已经在调用构造函数了,但是如果是自定义类型没有默认的构造函数就必须要用初始化列表。

class A
{
public:
    // 需要参数的构造函数
    A(int a): _a(a) {}
private:
    int _a;
}

class B
{
public:
    B(int b) : _b(b) {}// 错误
private:
    int _b;
    A _a;
}

👆这错误的写法,因为在A _a声明的时候,这是能能调用的。这就像如果A类的构造函数需要一个参数你可以直接A _a吗?不可以吧,这时必须要用A _a(2)括号里面必须要有一个参数,才可以传参。

总结:在成员数据中有const成员或者引用成员或者没有默认构造函数的自定义成员时,必须要用初始化列表初始化。而且如果可以的话,初始化变量的时候都用初始化列表的方式,这样可以效率更高也不容易出错。

还有最后一个小的注意细节:就是成员变量在初始化列表中的初始化顺序。

注意: 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

class A {
public:
 A(int a)
 :_a1(a)
 ,_a2(_a1)
 {}
 
 void Print() {
 cout<<_a1<<" "<<_a2<<endl;
 }
private:
 int _a2;
 int _a1; }
int main() {
 A aa(1);
 aa.Print();
}

上面这个程序的会出现什么?答案是输出1和随机值因为在成员变量声明的时候是_a1先声明 _1再声明,所以在初始化列表中是 _a2先初始化,所以a2是随机值。这是初始化列表中的初始化顺序的一个小细节

String类(带指针的类)

String类和Complex类还是不一样的,在String类中需要带一个指针方便动态的开辟空间。

String类中一定要写构造函数,因为要给动态开辟一段空间,所以在构造函数中一定要写开辟空间,如果数据成员中还有其他的成员数据还要给内置类型的数据初始化,自定义类型的数据成员还是会自动调用自己的构造函数

class String
{
	String(const char* str = 0)
	{
		if (str)
		{
			_data = new char[strlen(str) + 1];// 开辟空间留下一个位置给'\0'
			strcpy(_data, str);// 将内容拷贝到_data中
		}
		else// 如果str为空字符串就默认是'\0'
		{
			_data = new char[1];
			*_data = '\0';
		}
	}
private:
	char* _data;
};

总结:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cnu46PZ5-1622384260191)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528214858408.png)]

析构函数

每个类创建对象后,使用完之后都要对象中的内容清空,所以析构函数就出现了

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而

对象在销毁时会自动调用析构函数,完成类的一些资源清理工作

析构函数特征如下:

  1. 析构函数名是在类名前加上字符 ~。

  2. 无参数无返回值。

  3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

Complex类(不带指针的类)

class Complex
{
public:

	Complex(int real = 0, int image = 0)
	{
		_real = real;
		_image = image;
	}
	// ~ + 类名
	~Complex()// 析构函数
	{
		// 中间什么都不用写
	}
private:
	int _real; // 实部
	int _image;// 虚部
};

String类(带指针的类)

class String
{
	String(const char* str = 0)
	{
		if (str)
		{
			_data = new char[strlen(str) + 1];// 留下一个位置给'\0'
			strcpy(_data, str);
		}
		else// 如果str为空字符串就默认是'\0'
		{
			_data = new char[1];
			*_data = '\0';
		}
	}

	~String()// 析构函数记得要释放内存
	{
		delete[]_data;
	}
private:
	char* _data;
};

在Complex类的析构函数中什么都不用写,是不是又感觉析构函数也没啥用,可是在String类中析构函数要释放刚刚动态分配的内存,而且和构造函数类似如果类中有自定义类型的数据,他就会自动的调用自己的析构函数

class String
{
public:
	String(const char* str = 0)
	{
		if (str)
		{
			_data = new char[strlen(str) + 1];// 留下一个位置给'\0'
			strcpy(_data, str);
		}
		else// 如果str为空字符串就默认是'\0'
		{
			_data = new char[1];
			*_data = '\0';
		}
	}

	~String()// 析构函数记得要释放内存
	{
		delete[]_data;
	}
private:
	char* _data;
};

class A
{
private:
	String _str;// 自定义数据默认调用自己的析构函数
	int _a;
};

int main()
{
	A a;
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NSSYqM1A-1622384260191)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210528221125165.png)]

拷贝构造函数

在对象的构建中,我们经常会对一个对象进行复制拷贝,需要一个和某个对象一模一样的对象,所以在类中默认就会生成的函数中就包括了拷贝构造函数。

看名字就知道拷贝构造函数也是一种构造函数,也就是说他的作用是给一个对象初始化。

注意的两个点:

  1. 拷贝构造函数是构造函数的一个重载形式

  2. 拷贝构造函数的参数只有一个必须使用引用传参,使用传值方式会引发无穷递归调用

Complex类(不带指针的类的拷贝)

class Complex
{
public:
    // 缺省的构造函数
    Complex(int real = 0, int image = 0)
	{
		_real = real;
		_image = image;
	}
	// 传递的参数是对象c的引用
	Complex(const Complex& obj)
	{
		_real = c._real;
		_image = c._image;
	}

private:
	int _real; // 实部
	int _image;// 虚部
};

重点:一定要用const + 对象 + 引用来传值,如果用普通的传参拷贝就会引发递归死循环,因为如果是普通的传参,其实传进函数中的对象obj是函数外调用者对象的一个临时拷贝,这是就会又引发obj的拷贝构造,在拷贝构造的参数传进来之前又引发拷贝构造,这样下去就是一个死循环。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y1KoYkJu-1622384260192)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210529231005511.png)]

所以为了解决这个问题,那传参数的时候,就不让参数再调用构造函数,而是用引用给传进来的对象起一个“别名”就可以啦。而且因为是出入一个引用,为了保护类外调用的被引用的对象的安全,所以要加一个const保护一下,防止对象会被人修改。

拷贝构造函数的使用

两种方式都是一样的

Complex c1;
Complex c2(c1);// 第一种
Complex c3 = c1;// 第二种

以上两中方式都是可以的。

浅拷贝(值拷贝)

若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷

贝,这种拷贝我们叫做浅拷贝,或者值拷贝。

**也就是说如果在没有指针的类中,其实默认生成的构造函数就和我们显式写出来的拷贝构造函数是一样的。**都可以达到一样的效果。

String类(带指针的拷贝构造)

如果是在类中有指针的类中,还是用系统生成的默认拷贝构造函数,浅拷贝是完不成我们的期望的。

class String
{
	String(const char* str = 0)
	{
		if (str)
		{
			_data = new char[strlen(str) + 1];// 留下一个位置给'\0'
			strcpy(_data, str);
		}
		else// 如果str为空字符串就默认是'\0'
		{
			_data = new char[1];
			*_data = '\0';
		}
	}

	~String()// 析构函数记得要释放内存
	{
		delete[]_data;
	}
private:
	char* _data;
};

int main()
{
    String obj1("hello");
    String obj2(obj1);
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WSHlF7a6-1622384260192)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210529232654071.png)]

浅拷贝使得带指针的对象只是将指针复制过去了而已,所以只能得到一个对象obj2的指针和对象obj1的指针所指向的对象是同一个对象,而不是将指针所指向的内容一起复制,而且最严重的是因为两个指针同时指向一块空间,所以在调用析构函数的时候,两个对象会对同一块空间释放两次内存,这就是常说的“浅拷贝问题”。

解决方法其实很简单:进行深拷贝。即将指针指向的内容也复制一份,然后让obj2的指针指向被复制的区域即可。

inline
String(const String& str)
{
    // 开辟和str中_data一样的空间大小,+1是为了放结尾的'\0'
    _data = new char[strlen(str._data) + 1];
    // 复制内容
    strcpy(_data, str._data);
}

总结:

1)在不带指针的类中(如Complex类),在调用拷贝构造时候值拷贝(浅拷贝)就可以满足复制一个对象的效果了。所以在这种类的内部可以不用显式的写出构造函数,可以直接用类中默认生成的构造拷贝。

2)在带指针的类中(如String类),在复制另一个对象的时候需要的”深拷贝“,所以必须要手动的开辟另一块空间并将原来的内容复制到这块空间中,否则程序就会因为在同一块空间析构两次二崩溃。

拷贝赋值运算符重载

运算符重载

运算符重载就是赋予了运算符的意义。在默认的系统中,内置类型是有运算符可以直接比较的(+,-,*,/…),但是一个同种的对象中怎么比较呢?当然你可以写一个函数来实现你的目的。如果你想将两个Complex对象相加,你可以这样:

Complex& Add(Complex obj1, Complex obj2)
{
    // 注意一个小语法:类名(/*数据*/)创建一个临时对象
    // 也是说这个对象的生命期就在这5行上,到第6行的时候就自动销毁了
    Complex(obj1._real + obj2._real, obj1._img + obj2._img);
}

但是这样不够直观,如果可以这样obj1 + obj2,不是更只直观嘛。所以C++为增加代码的可读性,就增加了运算符重载。

函数名字为:operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

注意:

1)运算符重载不能创造一个新的元素符(如@…)

2)内置类型的操作符不可以改变原来操作符的意义。

3)作为成员函数重载运算符的时候,有一个默认形参this,而且限定为第一个函数形参

4). * (点,星号)、:: (冒号冒号)、sizeof 、?:(问号冒号) 、.(点) 注意以上5个运算符不能重载

拿Complex的==好举个栗子:

class Complex
{
public:

	// 函数重载
	Complex(int real = 0, int image = 0)
	{
		_real = real;
		_image = image;
	}
private:
	int _real; // 实部
	int _image;// 虚部
};
// 错误
bool operator== (const Complex& c1, const Complex& c2)
{
	return c1._real == c2._real && c1._image == c2._image;
}

这样将函数写在类外不加任何东西是错误的写法,因为在类外的对象根本就不能直接访问自己的私有成员,所以这样就会报错。但是在后面的内容中我会介绍一种特殊的函数friend友元函数,这种函数可以打破类的封装,那样就可以访问私有成员了。或者还可以将运算符重载写在函数中也是可以的,这样还可以少写一个形参(在类中默认的参数this)

class Complex
{
public:

	// 函数重载
	Complex(int real = 0, int image = 0)
	{
		_real = real;
		_image = image;
	}
 //返回值  operator运算符()
	bool operator== (const Complex& c)// bool operator== (const Complex* this, const Complex& c)
	{
		return _real == c._real && _image == c._image;
	}
private:
	int _real; // 实部
	int _image;// 虚部
};

int main()
{
	Complex c1(1, 2);
	Complex c2(1, 2);
	// 因为==的优先级比较低,所以要加括号
    // c1就是调用函数的对象
	cout << (c1 == c2) << endl;
    cout << c1.operator(c2) << endl;// 和上面一样,但是为了可读性一般写成上面的样子
	return 0;
}

前置和后置的区别:多加一个占位符int

再谈拷贝复制运算符重载

在运算符中有一种特殊的运算符—赋值号(=),这个运算符可以将一个对象的值赋值给另一个对象。

复制和赋值的区别:复制是在一个对象还不存在的时候,用一个对象初始化另一个对象,但是赋值是两个对象都已经存在了,将一个对象的内容赋给另一个对象

Complex c2(1,2);
// 复制
Complex c1 = c2;// c1在初始化创建
// 赋值
Complex c3;//c3已经创建出来了
c3 = c2;

赋值运算符要注意四点:

  1. 参数类型

  2. 返回值

  3. 检测是否自己给自己赋值

  4. 返回*this

  5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

Complex类(不带指针的类的赋值)
class Complex
{
public:

	// 函数重载
	Complex(int real = 0, int image = 0)
	{
		_real = real;
		_image = image;
	}

	Complex& operator= (const Complex& c)
	{
		// 如果this和对象c是同一个对象,就直接返回
		if (this == &c)
			return *this;

		_real = c._real;
		_image = c._image;
		return *this;
	}
private:
	int _real; // 实部
	int _image;// 虚部
};

int main()
{
    Complex c1;
    Complex c2;
    c1 = c1;// 自己赋值给自己
    c1 = c2;
    return 0;
}

注意:

1)在进入赋值函数的时候,要先判断一下是不是赋值给自己,如果赋值给自己就没必要在进行重复的操作,而且在带指针的类中是必须要加这个判断的(后面讲解)。

2)如果不自己写一个拷贝复制运算符重载函数,其实在不带指针的类中是没有区别的,因为默认的拷贝复制运算符重载函数也是这样写的。

String类(带指针的类的赋值)

但是在带指针的类中的拷贝复制运算符重载实际上也是一个”浅拷贝“,所以又是将指针拷贝了一份,而指针指向的内容却没有,只是两个指针指向了同一块空间。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-axyGjJ9z-1622384260193)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210530220936736.png)]

这时候只能是深拷贝来解决这个问题了,但是对于初学者来说可能有些复杂,所以只是贴一个代码,以后我会再写一个深拷贝的讲解


class String
{
	String(const char* str = 0)
	{
		if (str)
		{
			_data = new char[strlen(str) + 1];// 开辟空间留下一个位置给'\0'
			strcpy(_data, str);// 将内容拷贝到_data中
		}
		else// 如果str为空字符串就默认是'\0'
		{
			_data = new char[1];
			*_data = '\0';
		}
	}

	String& operator=(const String& str)
	{
		if (this == &str)
			return *this;

		// 删掉原来的空间
		delete[] _data;
		// 开辟一个和str一样的大小空间
		_data = new char[strlen(str._data) + 1];
		strcpy(_data, str._data);
		return *this;
	}

private:
	char* _data;
};

总结:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mFbq7oHu-1622384260193)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210530215017569.png)]

在带指针的类中一定要自己写一个拷贝复制运算符重载函数,以避免浅拷贝的问题。

从面向过程到面向对象之类的其他特殊成员

在这里插入图片描述

const成员

const修饰规则

const修饰是让一个变量具有常属性,也就是具有不可修改性

先尝试几道题目,回顾一下const基本用法:

一下const修饰的是*p,还是p?

int t = 10;
const int* p = &t;
int* const p = &t;
int const *p = &t;

答案如下:

int t = 10;
const int* p = &t;// const修饰*p,所以*p不可修改
int* const p = &t;// const修饰p,所以p的指向不可以改变
int const *p = &t;// const修饰*p,所以*p不可修变

**上面介绍了一些const修饰的规则,其实只要分清出const修饰的变量是谁,const在 * 前面修饰的就是 p,这时 p也就是p指针所指向的内容不可以被改变。如果const在 * 的后面修饰的就是p指针,这时p的指向就不可改变。

下面在谈谈int和const int级int* 和const int*之间的相互转化规则。

int a = 0;
const int b = 0;
a = b;// 权限的缩小,int -> const int
b = a;// 权限的放大,错误,const int -> int

int t = 0;
int *a = &t;
const int* b = &t;
a = b;// 权限的缩小,int* -> const int*
b = a;// 权限的放大,错误,const int* -> int*

总结和注意:

1)在const修饰变量的非const修饰变量之间相互转化遵循权限的缩小,不满足权限的放大。

const修饰的对象是只读不可写的,普通的对象是既可读也可写的,所以非const->const不可以const->非const

2)在const对象初始化的时候是不满足上面的情况的,初始化的时候比较特殊,所以这时可以对const用非const对象赋值也可以用数值赋值。

const修饰成员函数(修饰this)

const也可以在类中使用,可以在类中修饰成员函数,这个和上面用const修饰一个对象看起来好像不同,用const修饰一个函数是什么意思,可以对函数起什么作用?

其实const修饰成员函数不是修饰函数,还记得上文所说的每个成员函数是放在公共代码段中的,所以为了找到定义的对象需要用一个this指针(指向对象的指针)来找到定义的对象,从而来对定义的对象进行操作。这里const修饰的成员函数其实就是在用const修饰成员函数中隐藏的this指针,通过const修饰this指针就可以让this指针指向的东西不可修改,也就是让对象的数据不可修改这样就可以一定程度上保护对象的数据安全。

const修饰成员函数的写法:

class Complex
{
public:
    void fun() const // 将const加在函数的后面
    {
        cout << "const修饰的成员函数" << endl;
    }
private:
	int _real; // 实部
	int _image;// 虚部
};

const修饰成员函数不仅可以保护成员数据不被破坏之外,还有另一个用处。因为在类外const定义的对象,是只能调用const成员函数的,所以如果想要让const对象调用的函数,必须在类中用const修饰否则const对象不可以调用该成员函数。但是普通的对象是可以调用const修饰的成员函数的。

class Complex
{
public:
	void ShowData1()       // 普通成员函数
	{
		cout << _real << endl;
		cout << _image << endl;
	}
	void ShowData2() const // const修饰成员函数
	{
		cout << _real << " " << _image << endl;
	}
private:
	int _real = 1; // 实部
	int _image = 1;// 虚部
};


int main()
{
	Complex c1;
	const Complex c2;
    
	c1.ShowData1();// 非const对象调用非const修饰函数
	c1.ShowData2();// 非const对象调用const修饰函数
	c2.ShowData1();// 错误,const对象不能调用非const修饰函数
	c2.ShowData2();// const对象只能调用const修饰函数
    
    return 0;
}

总结:

1)const修饰成员函数其实修饰的是this指针

2)const修饰成员函数的意义:

1.保护对象的数据成员

2.const只可以调用const修饰的成员函数

看完之后思考一下:

问题:

  1. const对象可以调用非const成员函数吗?(不可以)

  2. 非const对象可以调用const成员函数吗?(可以)

  3. const成员函数内可以调用其它的非const成员函数吗?(不可以)

  4. 非const成员函数内可以调用其它的const成员函数吗?(可以)

友元

上面说过了运算符重载,但是有一种特殊的运算符没有谈到,就是<<>>标准输入流和标准输出流运算符的重载。

这两个运算符的重载比较特殊,首先要说明的是cincout本身就是对象,在C++标准中写明了他们分别属于istreamostream这两个类,其实这两个类就是官方写出来的类而已,不用关心它的细节。只要知道他的作用是定义出的对象可以完成输入和输出的操作即可。

还是以Complex类为例,打印出一个复数的数据

class Complex
{
public:
	istream& operator>> (istream& in)// 输入函数
	{
		in >> _real >> _image;
		return in;
	}
	ostream& operator<< (ostream& out)// 输出函数
	{
		out << _real << '+' << _image << "i" << endl;
		return out;
	}
private:
	int _real; // 实部
	int _image;// 虚部
};

int main()
{
	Complex c1;

	c1 >> cin;  // 等价于 c1.operator>>(cin)
	c1 << cout; // 等价于 c1.operator<<(cout);

	return 0;
}

我知道你肯定有很多问题:

1)第一个为什么返回值是ostream&。其实这是为了连续的输入输出,如果返回值是void,那只能输入输出一次。

cin >> c1 >> c2;
cout << c1 << c2;

如果cin >> c1返回值是void那后面就会是void >> c2,这样就错了,cout << c1返回void,然后void << c2这样也是错误的。所以 要返回传入的cin或者cout

2)为啥c1 << cinc1 << cout这不是反了吗?这就是问题所在,因为是在类中重载的运算符,所以在类外使用重载函数的时候就要用对象‘’点‘’出成员函数,所以这就需要一个最普通的函数而且函数的形参分别是ostream类类型

class Complex
{
public:
	istream& operator>> (istream& in, Complex& c);// 输入函数声明
	ostream& operator<< (ostream& out, Complex& c);// 输出函数声明
private:
	int _real; // 实部
	int _image;// 虚部
};

ostream& Complex::operator<< (ostream& out, Complex& c)
{
	out << c._real << '+' << c._image << "i";
	return out;
}

istream& Complex::operator>> (istream& in, Complex& c)
{
	in >> c._real >> c._image;
	return in;
}

int main()
{
	Complex c1;
	cin >> c1;
	cout << c1 << endl;

	return 0;
}

利用上面的在类外实现的函数是不是就可以写出cin >> c1;cout << c1 << endl;· 这样对象就可以在cin``cout的右边了。但是还有一个问题就是函数外类外定义,所以类中的私有数据成员是访问不到的,所以面这种代码连编译都通过不了。

友元函数

于是C++有一种语法叫做友元函数,这种函数可以打破类的封装,也就是即使在类外也可以访问类中(私有,保护,公有)的数据,只需要在函数前面加上一个关键字friend就可以了。

用法:将友元函数的声明放在类中,然后在类外实现。

class Complex
{
public:
	friend istream& operator>> (istream& in, Complex& c);// 输入友元函数声明
	friend ostream& operator<< (ostream& out, Complex& c);// 输出友元函数声明
private:
	int _real; // 实部
	int _image;// 虚部
};

ostream& operator<< (ostream& out, Complex& c)// 友元函数的实现
{
	out << c._real << '+' << c._image << "i";
	return out;
}

istream& operator>> (istream& in, Complex& c)// 友元函数的实现
{
	in >> c._real >> c._image;
	return in;
}

int main()
{
	Complex c1;
	cin >> c1;
	cout << c1 << endl;
	return 0;
}

在类外实现的函数是不用用Complex::这样标识出是类中的函数的,一位友元函数只是在类中声明一下而已,友元函数和类中的成员函数是没有关系的。

相信经过重载这两个运算符的例子,将友元函数的作用和意义讲清楚了。

但是友元函数还有很多细节需要注意:

1)友元函数可以直接访问类的私有成员,它是定义在类外部普通函数,不属于任何类,但需要在类的内部声

明,声明时需要加friend关键字。

2)友元函数不是类的成员函数,所以也就没有this指针,而且因为const修饰成员函数是修饰this指针,所以const也不能修饰友元函数。

3)友元函数可以在类定义的任何地方声明,不受类访问限定符限制

友元类

除了友元函数还有一种友元就是友元类,比如说:A是B的友元类,说明A类中可以访问B中所有的友元函数。

class B;// 需要提前声明一下,因为B类在A类的后面,需要提前声明B类
class A
{
private:
	void funA()
	{
		cout << "A的成员函数" << endl;
	}
	friend class B;
};

class B
{
public:
	void funB()
	{
		objA.funA();// 直接使用A类对象的私有成员函数
		cout << "B的成员函数" << endl;
	}
private:
	A objA;
};


int main()
{
	B objB;
	objB.funB();
	return 0;
}

友元类和友元函数的注意点:

1)友元关系是单向的,不具有双向性。

例如:A是B的友元类,A可以访问B中的成员,但是B不可以访问A中的成员,如果想要让B也访问A中的成员,必须要在A类中将B设为A的友元类。

2)友元关系没有传递性。

A的友元类是B,B的友元类是C,也就是A可以访问B,B可以访问C,但是A不可以访问C。

最后提一点建议:一般的情况下不要使用友元函数,因为它会打破类的封装性,你也不希望你的类被你搞的支离破碎吧!

静态成员(static)

还有一种特殊的成员叫做静态成员。这和C语言中的静态成员用法差不多,其实就是在静态区上开一段空间给对象。一般的对象所占的空间是在栈上的,而在静态区中的对象声明的周期很长,一直回到程序结束静态区的对象所占的空间才会归还给系统。

在类中只要在成员函数或者成员数据的前面声明为static,类成员称为类的静态成员,静态的成员变量一定要在类外进行初始化(这点要注意)

class Complex
{
private:
	static int _n;
};

int Complex::_n = 0;// 必须要在类外定义

感觉好像静态成员没有用武之地,但是有一个面试题就要用这个知识点:

面试题:请设计一个类,计算出程序中创建出了多少个类对象

思路:创建了多少个对象,其实就是在问在程序运行中调用了构造函数和拷贝构造函数一共多少次?因为在类外没有办法访问类中的情况,所以可以在类中定义一个变量专门记录这两个构造函数的调用次数,但是一般的成员数据会在每次创建对象的时候,重新分配给每一个对象,所以这时候静态成员的数据就起到作用了,因为静态成员只会在创建一次,所以可以一直记录函数调用的次数。

还有最后一个问题就是如何让获取静态的成员数据

1)将静态成员数据设置成public,然后通过类名::访问,但是这样破坏了类的封装性

2)写一个静态成员函数,专门返回静态的成员数据。

静态成员数据:形如:static void fun() {}

注意两点:

1)静态成员函数没有this指针,static不算类的成员函数。

2)静态成员函数因为没有this指针,所以不能访问非静态的成员。

class A
{
public:
	A() { _n++; }
	A(const A& a) { _n++; }
	static int CountA() { return _n; }// 返回静态成员数据
private:
	static int _n;
};

int A::_n = 0;// 必须要在类外定义

int main()
{
	A a[10];// 创建10个对象
	cout << A::CountA() << endl;// 输出创建对象的个数
	return 0;
}

用一张表格了解静态成员函数和普通成员函数的区别:

静态成员函数普通成员函数
所有对象共享
隐含this指针×
访问类中普通成员×
访问类中静态成员
通过类名直接调用×
通过对象直接调用

因为静态成员函数比较特殊可以用类名直接调用静态成员函数。而且静态成员数据是独立于类的,所以当类中只有静态成员数据的时候,相当于空类。

class Complex
{
private:
	static int _n;
};

int Complex::_n = 0;// 必须要在类外定义

int main()
{
    cout << sizeof(Complex) << endl; // 相当于空类,空类的大小为1个字节
    return 0;
}

总结:

  1. 静态成员为所有类对象所共享,不属于某个具体的实例

  2. 静态成员变量必须在类外定义,定义时不添加static关键字

  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问

  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员

静态成员只能在类外初始化

只能在该cpp所在的编译模块中使用该变量(static限制了变量的作用域)

static函数唯一能够访问的就是static变量或者其他static函数

explicit关键字与隐式转换

隐式转换

隐式转换经常可以看见,比如:

int a = 10;
a = 2.2;// 2.2是double,在这里转换成为了int类型

隐式转换的原理就是创建另一个临时的变量,将临时变量赋值给了a,并且临时对象是具有常属性的。所以所上面的2.2其实是创建出了一个int类型的2,然后再将这个2赋值给a。

int t = 0;
const double& d = t;// 正确
double& d = t;// 错误

上面这个例子就可以说明int类型的t在转换成double类型的时候,创建了临时变量。因为我们用double的引用给t起别名,但是double类型不能给t起别名,因为赋值给double&的是创建的临时变量,而这个临时的变量是具有常属性的,所以用普通的引用是不可以引用它的。如果这时用const double&就可以引用t。

既然内置类型的数据之间可以相互转化,那么内置类型可不可以转换成为类对象呢?

实际上是可以的,但是有一定的条件,如果一个类的构造函数的参数是单个参数(记住一定是类中只有一个参数的时候才可以),就可以有隐式转换,比如:

class Complex
{
public:
	Complex(int real) :// 只有一个参数
		_real(real) {}
private:
	int _real; // 实部
};

int main()
{
	Complex c(1);// 创建一个对象
	c = 2;// 2 隐式转换成为 Complex类型
	return 0;
}

对象的隐式转换:

Complex c = 2;
// 进行了以下的两步
Complex tmp(2);//先将2进行通过构造函数创造一个临时对象
Complex a(tmp);// 再将a通过赋值拷贝进行赋值

explicit

有的时候隐式转换会使得代码的可读性降低,因为类型的转换会使得代码的逻辑很乱。所以如果不想要让对象进行隐式转换就要用explicit声明一下,这样编译器就会让对象的隐式转换不发生。

class Complex
{
public:
	explicit Complex(int real) :// 只有一个参数
		_real(real) {}
private:
	int _real; // 实部
};

int main()
{
	Complex c(1);
	c = 2;// 错误,不能发生隐式转换
	return 0;
}

匿名对象

有一个小的语法点,就是创建对象的方式

class A
{
public:
    A(int a):_a(a) {}
private:
    int _a;
}

int main()
{
    A a1(1);// 构造函数
    A a2 = 2;// 隐式转换=构造函数+复制拷贝
    A(3);// 匿名对象
    return 0;
}

匿名对象:类名(/* 参数 */),记住匿名对象的声明周期很短,它只在创建匿名对象的这一行上有效。

所有有的时候,如果创建一个对象,而这个对象只需要用一次的话,创建一个匿名对象可以事半功倍。

构造函数带默认参数(C++11新语法)

上文说过构造函数对内置类型的数据不处理,对自定义类型会调用它的构造函数。C++中为了对内置类型不处理有一个专门的解决的方法。就是构造函数带默认的参数

class Complex
{
public:
	void Show()
	{
		cout << _real << ' ' << _image << endl;
	}
private:
	int _real = 0; // 实部缺省值
	int _image = 1;// 虚部缺省值
};

int main()
{
	Complex c;
	c.Show();// 0 1
	return 0;
}

在没有给成员数据显式的初始化的时候,构造函数就用默认的缺省值,赋值(不是初始化) 给成员数据。

内存管理(new和delete)

要介绍内存管理,首先先来一张内存的分布图。

在这里插入图片描述

看完上面的图,再上几道题目看看是否理解到位了:

int globalVar = 1;

static int staticGlobalVar = 1;

void Test()
{
	static int staticVar = 1;

	int localVar = 1;

	int num1[10] = { 1, 2, 3, 4 };

	char char2[] = "abcd";

	const char* pChar3 = "abcd";

	int* ptr1 = (int*)malloc(sizeof(int) * 4);

	int* ptr2 = (int*)calloc(4, sizeof(int));

	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);

	free(ptr1);

	free(ptr3);

}
lobalVar在哪里?静态区  staticGlobalVar在哪里?静态区

staticVar在哪里? 静态区  localVar在哪里? 栈区

num1 在哪里?栈区
    
char2在哪里? 栈区   *char2在哪里? 常量区

pChar3在哪里?栈区    *pChar3在哪里? 常量区

ptr1在哪里?栈区   *ptr1在哪里? 静态区

sizeof(num1) = 40;  

sizeof(char2) = 5;   strlen(char2) = 4;

sizeof(pChar3) = 4/8;   strlen(pChar3) = 4;

sizeof(ptr1) = 4/8;

C语言中内存管理(malloc和free)

如果了解过malloc和free,那你可以问自己几个问题:

malloc,calloc,realloc的区别
void compareMCR()
{
	int* p1 = (int*)malloc(sizeof(int));
	int* p2 = (int*)calloc(10, sizeof(int));
	int* p3 = (int*)realloc(p1, sizeof(int) * 10);

	free(p2);
	free(p3);
}

1.malloc在堆上动态开辟空间

2.calloc在堆上开辟空间,并且将空间上的值初始化为0(等价于malloc+memset()初始化为0)

3.realloc对已有的空间扩容(1.原地扩容 2.异地扩容)

C++中的内存管理(new和delete)

new和delete的两种用法:

1)申请单个空间 new 类型,删除单个空间delete 指针

2)申请多个空间new 类型[/* 空间大小(可略) */] delete[]指针,这种申请多个空间的方式也叫做array new和array delete

内置类型申请空间
void testPtr()
{
	// 申请单个空间
	int* p1 = (int*)malloc(sizeof(int));
	free(p1);
	int* p2 = new int;
	delete p2;
	// 申请多个空间
	int* p3 = (int*)malloc(sizeof(int) * 10);
	free(p3);
	int* p4 = new int[10];
	delete[] p4;
}
自定义类型申请空间

自定义类型使用new的时候new会自动调用对象的构造函数,如果对象中有成员数据可以用new的时候初始化new 类型()或者new 类型[]{/* 多个成员初始化 */}(C++新语法)

class A
{
public:
	A(int a, int n) : _a(a), _n(n) {}// 构造函数
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
	int _n;
};

int main()
{
	A* p1 = new A{1, 2};// 自动调用构造函数,p的值为1
	delete p1;          // 自动调用析构函数,输出~A()

	// 自动调用构造函数2次。
	// 初始化两个对象,其余的对象初始化为0
	A* p2 = new A[2]{ {1, 1}, {2, 2} };
	delete[] p2;        // 自动调用析构函数2次
	return 0;
}

array new 和 array delete内存分布图

在这里插入图片描述

A* p = new A[3];
delete[] p;// 唤起了3次析构函数
A* p = new A[3];
delete p;// 只唤起来了1次析构函数

在这里插入图片描述

第二个操作只进行了一次析构所以导致了内存泄露,但是一块空间还是被回收的

operator new / operator delete

new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间然后再调用构造函数,delete在底层先调用析构函数然后通过operator delete全局函数来释放空间。

operator new / operator delete和malloc/free的区别:

new/delete可以调用构造函数和析构函数,但实际上operator new/operator delete和malloc/free使用的方法是一样的都是在堆上申请空间和释放空间。但是唯一有一个不同点就是如果空间申请失败的话,malloc和operator new处理的方式不同。

malloc申请空间失败会返回空指针。operator new 申请空间失败的话会抛出异常(异常会再后续的继承和多态的博客中写)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8KakpSWv-1622984616783)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210606203602801.png)]

void * p1 = malloc(0x7fffffff);// 2G的空间
if (p1 == NULL)// 判断是否返回空指针
{
	cout << "malloc failed" << endl;
}
free(p1);

try// 检查异常
{
	void* p2 = operator new (0x7fffffff);
	delete p2;
}
catch (const std::exception& e)
{
	cout << e.what() << endl;
}

总结:new/delete和malloc/free的区别

1)针对内置类型的数据申请空间没有任何差别

2)对于自定义类型的数据,new/delete会自动调用对象的构造函数和析构函数。但是malloc/free不会。malloc/free只是单纯的在堆区上开辟空间和释放空间,而new/delete则会在开空间后初始化对象的数据,在释放空间之前会清理对象的数据。

3)new/delete和new[]/delete[]一定要匹配使用,否则对于内置类型的空间释放没有影响,对于自定义类型的对象就会程序崩溃,并且会有内存泄漏的问题。

4)new失败会抛异常,malloc失败会返回NULL。

new和delete的实现原理

对于内置类型

对于内置类型的数据来说,new/delete和malloc/free基本相同,只是在申请空间上,如果申请>1的空间需要用new[],而且new失败会抛异常,malloc失败会返回NULL。

对于自定义类型

new 实现原理

1)operator new 申请空间(失败抛异常)

2)调用构造函数

delete实现原理

1)调用析构函数,对对象中的资源进行清理

2)operator delete释放空间

new[n] 实现原理

1)operator new[]调用operator new 申请空间为n个对象申请空间(失败抛异常)

2)调用X次的构造函数

delete[n]实现原理

1)调用n次的析构函数,对对象中的资源进行清理

2)operator delete[]释放空间


定位new表达式(placement new)

如果你要使用malloc或者operator new动态开辟一段空间存放对象,但是这时你只是开辟了空间但是没有调用对象的构造函数。所以定位new表达式的作用就体现出来了。

new表达式的作用:对一块已经分配空间的对象,调用其构造函数。

使用格式:

new (place_address) type或者new (place_address) type(initializer-list)

void testNewP()
{
	A* p = (A*)malloc(sizeof(A));// 开辟空间
	new(p)A{ 1, 2 };// (placement new)调用构造函数
	free(p);
	// 上面等价于 A* p = new A {1, 2}; delete p;
}

总结:

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

1.malloc和free是函数,new和delete是操作符

2. malloc申请的空间不会初始化,new可以初始化

3.malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常

4.申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

  • 9
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hyzhang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值