C++基础知识3

1.类对象大小

#include<iostream>
using namespace std;


class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date s1;
	Date s2;
	return 0;
}

类实例化出的每个对象,都有独立的空间,所以对象中肯定包含成员变量,而成员函数是不包含的,首先函数被编译后是一段指令,对象中没有办法存储,这些指令存储在一个单独的区域(代码段),对象要存储的话,只能是成员函数的指针。而对象中是否有存储指针的必要,Date实例化s1和s2俩个对象,s1和s2都有各自独立的成员变量,_year,_month,_day存储各自的数据,但是s1和s2成员函数Init和Print指针却是一样的,如果每个都开一个空间给成员函数就浪费了。函数指针是不需要存储。

内存对齐规则

1.第一个成员在于结构体偏移量为0的地址处

2.其它成员要对齐到某个数字(对齐数)的整数倍的地址处

3.对齐数是编译器默认的一个对齐数于该成员大小的较小值(vs默认对齐数为8)

4.结构体总大小:最大对齐数(所有变量类型最大者与默认对齐数取最小)的整数倍

5.嵌套结构体的话,先算出里面结构体的大小,里面结构体的对齐数就是它内部成员对齐数最大的哪一个,最后结构体的整体大小就是所有最大对齐数的整数倍。

例子:

class T
{
	char _ch;
	int i;
};

类的成员有char型的_ch和int型的i,第一个是char则在偏移量为0的地址处,大小为1,int对齐数为4,要对齐到偏移量为4倍数的位置,就要到偏移量为4位置, 此时占了八个字节,类的最大小要为最大对齐数的整数倍,成员对齐数最大为int(4),八为4的倍数,所以类的大小为八个字节。

 下面创建一个没有内容的类A,它的大小为1,因为如果一个字节不给,怎么去表示对象存在过,这里的1字节是为了占位标识对象存在。

class A
{

};
int main()
{
	Date s1;
	Date s2;
	T a;
	A b;
	cout << sizeof(b) << endl;
	return 0;
}

2.this指针

1.Date类中有Init与Print俩个成员函数,函数体中没有关于不同对象区分,当s1调用Init和Print函数时,该函数是如何知道访问的是s1对象还是s2对象呢,那么这里就看C++给了一个隐含的this指针解决问题

2.编译器编译后,类的成员函数默认在形参第一个位置,增加一个当前类型的指针,而这个指针this有指向这个变量成员的访问权限,那么就可以知道是哪一个对象去执行函数

void Init(Date* const this,int year,int month,int day)

3.类成员函数中访问成员变量都是通过this指针访问的,如给int _year赋值,

this->_year=year;

4.C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会自动处理),但是可以在函数体内显示使用this指针。

5.this指针存储在栈里面(寄存器)

#include<iostream>
using namespace std;


class Date
{
public:
	void Init(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	void Print()
	{
		cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date s1;
	Date s2;
	s1.Init(11, 22, 33);
	s1.Print();
	return 0;
}

 

 下面的代码,创建类的指针p指向nullptr,然后通过->去找到成员函数,需要注意的是空指针是不能解引的,但是这里->符号代表的不是解引的意思,它还有别的作用,这里是关于汇编的一些指令

,并不是解引,当p->_a=1就会出错了,因为这时->就是解引的意思了

#include<iostream>
using namespace std;

class a
{
public:
	void print()
	{
		cout << this << endl;
		cout << "a::print()" << endl;
	}
private:
	int _a;
};

int main()
{
	a* p = nullptr;
	// mov ecx  p
	p->print(); // call  地址
	//p->_a = 1;
}

下面的代码会出错,是因为Print里面有成员变量,但是类p是指向nullptr,所以会有空指针解引。

cout<<this->_a<<endl;

class a
{
public:
	void print()
	{
		
		cout << "a::print()" << endl;
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	a* p = nullptr;
	// mov ecx  p
	p->print(); // call  地址
	//p->_a = 1;
}

 

3.类的成员默认函数

 默认成员函数就是没有显示实现,编译器会自动生成的成员函数称为默认成员函数。一个类,在我们不写的情况下编译器会默认生成一下六个默认成员函数

 3.1构造函数

构造函数是特殊的成员函数,构造函数的主要任务不是开辟空间创建对象,而是对象实例化时初始化对象。构造函数本质是代替以前Stack中Init函数的功能,构造函数自动调用的特点就很好的代替了Init。

构造函数的特点:

1.函数名字与类的名字一样

2.无返回值。(函数名字前面的类型也不用写,C++规定)

3.对象实例化时系统会自动调用对应的构造函数

4.构造函数可以重载

5.如果没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦有显式定义编译器就不会自动生成

6.无参数构造函数,全缺省构造函数,不写显式构造函数编译器默认生成的构造函数,都叫为默认构造函数。但是三个有且只有一个能存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。不是编译器默认生成的叫默认构造,实际上无参构造和全缺省构造函数也是默认构造。

7.编译器默认生成的构造,对内置类型成员变量的初始化没有要求,初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果成员变量没有默认构造函数就会报错。(就是创建了一个有参数的构造函数,但是仅有这一个,又没有初始化自定义类型变量就会报错)

#include<iostream>
using namespace std;

class date
{
public:
	 //1.无参构造函数
	//date()
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}
 // 

	 2.带参构造函数
	//date(int year, int month, int day)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

	// 3.全缺省构造函数
	/*date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/

	void print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	date d1;
	d1.print();

	

	/*date d3(2024);
	d3.print();*/

	//date func();
	//func.print();

	return 0;
}

编译器会自动调用MyQueue的默认构造函数,这个构造函数又会去调用其的成员Stack的默认构造函数 

#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

// 两个Stack实现队列
class MyQueue
{
public:
	// 编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
	// 编译器默认生成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源
	// 显示写析构,也会自动调用Stack的析构
	~MyQueue()
	{
		cout << "~MyQueue()" << endl;
	}
private:
	Stack pushst;
	Stack popst;
	//int size;
};

int main()
{
	MyQueue mq;
	//Stack st1;
	//Stack st2;

	return 0;
}

 

 

补充:

对于成员变量的初始化,C++中确实有一些规则和行为:

1.内置类型成员变量的初始化:


2.如果你没有显式初始化内置类型的成员变量,在编译器默认生成的构造函数中,它们的值是未定义的(即可能是随机值或者未初始化的状态)。编译器不会自动为内置类型成员变量进行初始化。


3.自定义类型成员变量的初始化:


4.当你有一个自定义类型的成员变量时,编译器会尝试调用该类型的默认构造函数来初始化这个成员变量。如果该自定义类型没有默认构造函数(即没有参数的构造函数),并且你没有显式提供初始化方式,则会导致编译错误。因此,如果你自定义了一个类并将其作为成员变量,但没有默认构造函数,你必须提供初始化方法,否则编译器无法生成默认构造函数。

下面是一个示例来说明这些行为:
#include &lt;iostream&gt;

// 自定义类没有默认构造函数
class NoDefaultConstructor {
public:
    NoDefaultConstructor(int value) {
        std::cout &lt;&lt; "NoDefaultConstructor constructor called with value: " &lt;&lt; value &lt;&lt; "\n";
    }
};

// 包含内置类型和自定义类型成员变量的类
class MyClass {
private:
    int integer; // 内置类型成员变量
    NoDefaultConstructor custom; // 自定义类型成员变量

public:
    MyClass() {
        // 如果没有显式初始化 integer,其值是未定义的
        std::cout &lt;&lt; "MyClass default constructor called\n";
    }
};

int main() {
    MyClass obj; // 创建 MyClass 对象
    return 0;
}

在这个例子中:

5.MyClass 包含了一个 int 类型的成员变量 integer 和一个 NoDefaultConstructor 类型的成员变量 custom。
6.NoDefaultConstructor 没有默认构造函数,因此在 MyClass 的构造函数中必须显式初始化 custom,否则编译器会报错。

总结来说,C++中对于自定义类型的成员变量,确实要求调用其默认构造函数进行初始化,否则会导致编译错误。对于内置类型的成员变量,编译器不会自动进行初始化,它们的值是未

 

 3.2析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,就释放了,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类似Destory函数的功能。

析构函数的特点:

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

2.无参数无返回值,函数名字前面不用加类型

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

4对象生命周期结束时,系统会自动调用析构函数

5.跟构造函数类似,编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用它的析构函数

6.显式写析构函数,对于自定义类型成员也会调用它的析构函数,也就是说自定义类型成员无论什么情况都会自动调用析构函数

7.如果类没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果编译器生成的析构函数可以用也可以不写显式析构,如MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄露,如Stack。

如果有俩个Stack要先析构哪一个呢,跟栈很像后进先出,最后的先析构再到前面的,而类里面包类的则外面的先析构再到成员析构。

#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	// ...

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

// 两个Stack实现队列
class MyQueue
{
public:
	// 编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始化
	// 编译器默认生成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源
	// 显示写析构,也会自动调用Stack的析构
	~MyQueue()
	{
		cout << "~MyQueue()" << endl;
	}
private:
	Stack pushst;
	Stack popst;
	//int size;
};

int main()
{
	MyQueue mq;
	Stack st1;
	Stack st2;

	return 0;
}

补充:

在C++中,如果你显式定义了一个类的析构函数,无论这个类是否包含其他自定义类型的成员变量,当对象被销毁时,它的析构函数会被自动调用。这也适用于成员变量的析构函数。让我们通过一个示例来说明这一点:
假设我们有一个自定义的类 Member 和一个包含该类对象的类 Container,并显式定义了析构函数。
#include &lt;iostream&gt;

class Member {
public:
    Member() {
        std::cout &lt;&lt; "Member constructor called\n";
    }

    ~Member() {
        std::cout &lt;&lt; "Member destructor called\n";
    }
};

class Container {
private:
    Member member;

public:
    Container() {
        std::cout &lt;&lt; "Container constructor called\n";
    }

    ~Container() {
        std::cout &lt;&lt; "Container destructor called\n";
    }
};

int main() {
    Container c; // 创建一个 Container 对象
    return 0;
}

在这个例子中:

1.类 Member 有一个构造函数和一个析构函数,它们会在对象创建和销毁时输出相应的消息。
2.类 Container 包含一个 Member 类型的成员变量 member。
3.在 main 函数中,创建一个 Container 对象 c。

当程序运行时,输出将会是:
Member constructor called
Container constructor called
Container destructor called
Member destructor called

这表明,在 Container 对象 c 被销毁时,首先调用 Container 的析构函数,然后调用 Container 的成员 Member 的析构函数。这是因为当一个对象被销毁时,它的成员对象也会相应地被销毁,即调用其析构函数。
因此,无论是否显式定义了析构函数,自定义类型的成员变量在对象销毁时都会自动调用它们的析构函数。

  • 49
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值