C++学习笔记 (2)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

C++问题的补充

前言

关于对之前遗留的补充

  • malloc 和 new 的区别
  • const 和 引用 的深入
  • this指针 的深入

一、C++中对象的创建

malloc和new创建对象

//定义一个Pointer类
class Pointer
{
public:
	int row;
	int col;

	Pointer()
	{
		row = 0;
		col = 0;
	}

	Pointer(int r, int c)
	{
		row = r;
		col = c;
	}

	~Pointer()
	{
		cout << "Pointer" << endl;
	}
};

malloc不能创建对象,需要配合定位new使用

//什么是 定位new
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
换句话说就是,现在空间已经有了,不需要定位new像常规new一样去给申请
空间,只需要定位new在已有的空间上调用构造函数构造对象而已。
简而言之,已存在空间,再构造对象。

malloc作用:

  • 1、堆区申请空间
  • 2、堆区无对象
//定位new,在已有(malloc申请的)空间上,构造对象
int main()
{
	Pointer* s = (Pointer*)malloc(sizeof(Pointer));

	new(s) Pointer(12, 23); 

	s->~Pointer();
	free(s);
	s = NULL;
	return 0;
}

new创建对象的的过程:

  • 1、调用malloc,从堆区申请空间
  • 2、构建对象,调用构造函数
  • 3、对象地址空间返回给p
//普通new,先申请空间,再创建对象
int main()
{
	Pointer p1(100,200);
	Pointer p;
	p = new Pointer(10,20);

	delete p;
	return 0;
}

二、new 和 malloc 的区别

区别 new malloc
属性 关键字 库函数
参数 无需指定内存块大小,编译器实现 需要指定大小
返回类型 对象类型的指针 void *
重载 支持 不支持
内存 在free store上分配内存 在堆区空间分配内存
分配失败 bac_alloc异常 返回NULL
自定义类型 可以调用析构与构造 不可调用析构和构造
效率

三、C与C++的一个重要区别

C语言:有空间一定能操作
C++: 有空间不一定有对象,有对象一定要有空间

C

int main()
{
	char str[10];
	int* ip = (int*)str;
	*ip = 100;
}

C++:有对象一定要有空间

//有对象一定有空间,即使是空类
class Test
{

};

int main()
{
	Test t;
	cout << sizeof(t) << endl;	//空类大小为1
	return 0;
}

空类的大小:
声明类型时也需要占用一些空间
如果没有这个空间,也就不知道该类型的存在。
空类具体占用的空间大小由编译器决定,vs2022中大小为1
编译器自动为其安插1个char。

//有空间可能没对象
int main()
{
	CGoods* p = (CGoods*)malloc(sizeof(CGoods));
	cout << p->GetPrice() << endl;	//结果:-4.31602e+08
}

四、this指针

1. 什么是this指针

例子代码如下:

//一个商品类型
class CGoods
{
private:
	char Name[21];
	int Amount;
	float Price;
	float Total_value;
public:
	void RegisterGoods(const char[], int, float);	//注册商品
	void CountTotal();		//总共数量
	void GetName(const char[]);	//获得名字
	int GetAmount(void);		//读取商品
	float GetPrice(void);		//得到价格
	float GetTotal_value(void);	//总价
};

void CGoods::RegisterGoods(const char name[], int amount, float price)
{
	strcpy_s(Name, 21, name);
	//strcpy_s(this->Name, 21, name);
	Amount = amount;
	//this->Amount = amount;
	Price = price;
	//this->Price = price;
}

void CGoods::GetName(const char name[])
{
	strcpy_s(Name, 21, name);
}

int CGoods::GetAmount(void)
{
	return Amount;
}

float CGoods::GetPrice(void)
{
	return (Price);
}

int main()
{
	CGoods c1, c2, c3;
	c1.RegisterGoods("C++", 12, 98.99);
	c2.RegisterGoods("C++", 23, 15.9);
	c3.RegisterGoods("C++", 44, 54.3);

	return 0;
}

这里并没有this的存在,那么this指针在哪呢?
首先来看一下C++模型对象的安排

方法一:各对象完全独立的安排内存
image
上图是系统为c1,c2,c3对象分配了全套的内存,包括安放成员数据的数据区和安放成员函数的代码区。但是区别同一个类所实例化的对象是由属性(数据成员)的值决定,不同对象的数据成员的内容是不一样的,而行为(操作)是用函数来描述的,这些操作的代码对所有的对象都是一样的。

方法二:数据区私有,代码区公有

image

在C++中,通过公共代码区节省了大量的空间。
那么对象是如何区分哪个是自己的数据区呢?

this指针就是解决这个问题的。

2. this指针的底层

首先明确几个概念:

  • this指针不属于对象的一部分,不会影响sizeof(对象)的结果。
  • 只有在类的非静态成员函数才可以使用this指针,其它任何函数都不可以。
  • this的作用域在类成员函数的内部
    当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。

仍然是上一个例子:

//注释为本质
//成员方法的第一个参数添加一个对象类型的this指针。
//成员方法中出现的所有成员属性由this指针指向。
void CGoods::RegisterGoods(const char name[], int amount, float price)
//void CGoods::RegisterGoods(CGoods *this,const, const char name[], int amount, float price)
{

	strcpy_s(Name, 21, name);
	//strcpy_s(this->Name, 21, name);
	Amount = amount;
	//this->Amount = amount;
	Price = price;
	//this->Price = price;
}

int main()
{
	c1.RegisterGoods("C++", 12, 98.99);
	//对象 + . 调用成员方法的本质
	//逻辑上面向对象,实际上是面向过程
	//RegisterGoods(&c1, "C++", 12, 98.99);
}

这样就不难理解,当通过 对象 + . 调用成员方法时,会将对象的地址作为第一个参数传递给成员方法,这也是一个对象数据区的唯一标识。得到该地址以后,调用成员函数中如果遇到成员属性,就给每个属性前面加上this。

3.this指针 的类型

this指针的类型为 : 类型名 * const this
但是在VS2022中发现并没有这个const,不知道是什么情况

4. C++类成员函数默认调用方式 与 c语言调用方式的区别

C++默认调用(C++成员函数的this指针)

void CGoods::RegisterGoods(const char name[], int amount, float price)
{
	strcpy_s(Name, 21, name);
	//strcpy_s(this->Name, 21, name);
	Amount = amount;
	//this->Amount = amount;
	Price = price;
	//this->Price = price;
}

void CGoods::GetName(const char name[])
{
	strcpy_s(Name, 21, name);
}

int CGoods::GetAmount(void)
{
	return Amount;
}

float CGoods::GetPrice(void)
{
	return (Price);
}

int main()
{
	CGoods c1, c2, c3;
	c1.RegisterGoods("C++", 12, 98.99);
	//C++的调用约定 lea ecx , [c1]
	c1.CountTotal();
	
	return 0;
}

对象 . 调用成员方法,逻辑上是面向对象,物理上还是面向过程,会将对象地址传递给成员函数的this指针。对象的地址传递时候,直接进行 lea ecx, [c1]。到达成员函数内部时,使用mov指令,将ecx的值传递给this指针。
image

image

c语言调用方式(thiscall调用)

在成员函数前面加上__cdecl ,说明是C的调用约定,我们可以将c1的地址传递给eax,通过push指令入栈eax,直接传递给形参。

//通过使用__cdecl改写 thiscall
//void __cdecl CGoods::RegisterGoods(const char name[], int amount, float price);

image

image

5、this 与 常对象

常对象只能调用常方法,普通对象能调用普通方法和常方法。

例子:

//main函数中,哪些语句能编译通过
class CGoods		//设计类型,不是定义类型
{
private:
	char Name[21];
	int Amount;
	float Price;
	float Total_value;
public:
	CGoods(char name[], int amount, float price, float total_value)
	{
		strcpy_s(Name, name);
		Amount = amount;
		Price = price;
		Total_value = total_value;
	}
	void RegisterGoods(const char[], int, float);

	float GetPrice(void)
	{
		return Price;
	}
	float GetTotal_value(void) const
	{
		return Total_value;
	}
};

int main()
{
	CGoods c1 = { "C++",12,23.0,23.0 };
	const CGoods c2 = { "java",12,23 };

	c1.GetPrice();	//right:普通对象调用普通方法可以通过this指针修改成员函数的值
	c2.GetPrice();	//error:常对象不能调用普通方法。常对象调用普通方法时,成员属性有修改的风险,所以报错。

	c1.GetTotal_value();	//right:普通对象既可以调用普通方法又可以调用常方法
	c2.GetTotal_value();	//right:常对象只能调用常方法
}

我们知道了this指针的类型是 对象名 *const this。那么对象加 const 也就是const 对象名 *const this类型,为了给this指针所指向的对象限定,也就是不允许修改对象中的成员属性,那么任何对成员属性进行修改的行为都是错误的。

四、const 回顾

int main()
{
	const int a = 10;
	int b = 20;

	int* p1 = &a;			//error:可以通过p1改变a
	const int* p2 = &a;		//right:p2所指向的值不能修改
	int* const p3 = &a;		//error:可以通过p3改变a,但不能改变p3的指向
	const int* const p4 = &a;	//right:能力收缩
	return 0;
}

五、构造函数和析构函数

1.构造函数和析构函数的用法

class Pointer
{
public:
	int row;
	int col;
	
	//构造函数:
	//1、函数名与类名相同
    	//2、无返回值
    	//3、对象实例化时,编译器自动调用
    	//4、可以重载
		
	Pointer()		//无参构造函数,缺省构造
	{
		row = 0;
		col = 0;
	}

	Pointer(int r, int c)		//构造函数重载
	{
		row = r;
		col = c;
	}

	~Pointer()	//析构函数
	{
		cout << "Pointer" << endl;
	}
	
	//析构函数:
	//1、~ + 类名 
	//2、析构函数无函数返回值类型
	//3、一个类只能一个也只能有一个析构函数
	//4、对象注销时,系统会自动调用
	//5、如果没有给出析构函数,系统会给出默认析构函数
};

int main()
{
	Pointer c1 = { 12,23 };
	Pointer c2;		//right:调用缺省构造函数
	//Pointer c2();		//error:没有参数不需要带括号
	Pointer c3(10, 20);		//right:调用带参数构造函数

	//不能通过对象手动调用构造函数
	//c2.Pointer(10,20);	//error

	//有空间不一定有对象,得要对象调用构造函数来构建对象,才有空间
	//有对象一定有空间
	return 0;
}

2.构造函数和构造函数的执行顺序

留给后面

class Object
{
private:
	int val;
public:
	Object(int x)
	{
		val = x;
		cout << "create :" << val << endl;
	}
};
Object o1(1);

int main()
{
	Object o2(2);
}

Object o3(3);

已解决:程序执行顺序


总结

待解决的问题:

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++