C++类和对象

文章详细介绍了C++中的类和对象的概念,包括类的定义、成员函数、访问限定符、对象的实例化和大小,以及this指针的作用。接着讨论了构造函数和析构函数,强调了它们在对象生命周期中的初始化和清理作用。文章还深入探讨了拷贝构造函数和赋值重载的重要性,特别是在处理资源管理时的深拷贝和浅拷贝概念。此外,提到了const取地址操作符重载和初始化列表在对象初始化中的应用。


C语言关注的是过程,它是一门面向过程的语言,而C++是基于面向对象,关注的是对象,是面向对象的语言。

在C语言中,结构体只能定义变量,而C++引入了函数,另外C++更喜欢用class定义类:

class A
{
	int Add(int a, int b)
	{
		return a + b;
	}

	int x;
	int y;
};

类里面就是类的成员,成员中变量叫成员变量或者类的属性,函数叫成员函数或者类方法。
成员函数定义方式:
1、在类中定义,编译器可能会把它当成内联函数。
上面这个类的成员函数就是在类中定义的。
2、在类中声明,在其他文件定义。

//头文件:
#include<iostream>

using namespace std;

class A
{
	/*int Add(int a, int b)
	{
		return a + b;
	}*/
	int Add(int a, int b);

	int x;
	int y;
};
//源文件:
#include"test.h"

int A::Add(int a, int b)
{
	return a + b;
}

类的访问限定符

C++将变量及函数封装在一起,通过访问权限选择性的将接口提供给外部用户使用。
访问限定符有3个:
public:公有
private:私有
protected:保护
说明:

public是可以提供给外部用户使用的。
而其他两个不能在类外直接访问
class的默认权限是private,而struct默认权限是public(因为struct要兼容C)
访问权限作用域是该访问限定符位置到下一个访问限定符之间。

类的实例化

用类创建对象的过程就是实例化,一个类可以定义很多个对象,实例化出来的对象占物理空间,类是不占用空间的,可以把类当成一个施工图,而对象就是按照施工图做出来的房子。

class A
{
	int Add(int a, int b)
	{
		return a + b;
	}

	int x;
	int y;
};

int main()
{
	A a;//这个a就是A类实例化出来的对象
	return 0;
}

对象的大小

在类中的成员函数是不算在对象大小里的,它是放在公共代码段中,我们来看看下面这段代码:

class A
{
	int Add(int a, int b)
	{
		return a + b;
	}

	int x;
	int y;
};

int main()
{
	A a;
	cout << sizeof(a) << endl;//输出8
	return 0;
}

对象的大小是只算成员变量的,它和C中算结构体大小方式是差不多的,算大小时同样要注意内存对齐。

this指针

以前,我们在学C语言时,把一个结构体对象初始化时调用对应的自定义函数,需要把这个结构体对象传参传过去,但是在C++中不一样:

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;
	d.Init(2023, 3, 12);//并不需要把d这个对象传过去
	return 0;
}

因为C++在这个成员函数中新增一个隐式指针this,这个指针指向这个对象,在成员函数中对这个对象的操作都是用this解引用操作的,这个this指针不需要我们手动去写,编译器自动完成,这个this指针显示出来的效果就是这样的:

class Date
{
public:
	void Init(Date* this,int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date* ptr;
	ptr->Init(&ptr,2023, 3, 12);
	return 0;
}

this指针的类型是* const 类型,在成员函数中不能直接对this指针赋值。
this指针是成员函数的一个隐式参数,将对象的地址传递过去,是形参,所以它不是存在类里面的,它是存在寄存器里的。
那它可以为空吗?
先说答案:可以

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << "hello world" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date* d = nullptr;
	d->Print();
	return 0;
}

其实,对象是空不是最重要的,重要的是有没有解引用,像上面这种情况,虽然是空,但是没有解引用,只是打印,就没问题。

类的6个默认成员函数

之所以说它们是默认成员函数,是因为如果我们不写,编译器会自动生成。

初始化和清理

构造函数

它是一个特殊的成员函数,它是把每个成员变量都给一个初始值,并且在对象的生命周期内只能调用一次。
注意:构造函数不是赋值,而是初始化,并没有开空间。

特点:
1、函数名与类名相同。
2、没有返回值。
3、函数名可以构成重载。
4、对象实例化是自动调用构造函数,如果我们没写,编译器自动生成构造函数。

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << "hello world" << endl;

		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d(2023, 3, 14);
	d.Print();

	Date d1();//实例化对象时这样是不行的,这样就是一个函数的声明,且不需要传参,所以对象实例化时如果没有参数,就不要加括号
	return 0;
}

那既然我们不写默认构造函数,编译器可以自动生成,那自动生成的构造函数是怎么样的呢?

class Date
{
public:
	/*Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/

	void Print()
	{
		cout << "void Print()" << endl;

		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	//Date d(2023, 3, 14);
	Date d;
	d.Print();

	//Date d1();//实例化对象时这样是不行的,这样就是一个函数的声明,且不需要传参
	return 0;
}

该代码输出结果是随机值,结果是未定义的,为什么呢?
那是因为C++把类型分为两种。一种是内置类型,比如int、char这样的类型,还有一种类型就是自定义类型,我们自己定义的,像struct、class这样的就是自定义类型,默认生成的构造函数是只对自定义类型处理,内置类型不处理,而Date类的成员变量里都是内置类型,所以是随机值,我们再来看看自定义类型默认生成的是什么

class Time
{
public:
	Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}

	void Print()
	{
		//cout << "void Print()" << endl;

		cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		//cout << "void Print()" << endl;

		cout << _year << "年" << _month << "月" << _day << "日" << endl;
		this->t.Print();
	}
private:
	int _year;
	int _month;
	int _day;

	Time t;
};

int main()
{
	Date d(2023, 3, 14);
	//Date d;
	d.Print();

	return 0;
}

在这里插入图片描述
如果我把日期类和时间类的构造函数都注释掉,再来看看输出结果:
在这里插入图片描述
所以,其实说白了,内置类型的构造函数还是要写的,内置类型构造函数不写,就是随机值,那自定义类型初始化也就跟着变随机值了。

C++11针对内置类型不初始化就是随机值这个缺陷打了补丁:在成员变量声明时可以给定默认值,这个默认值不是赋值,是缺省值,不开额外的空间。

析构函数

它的功能和构造函数相反,它是一个资源清理的函数,它的特点和构造函数差不多,只是有一点点的区别:

1、析构函数函数名是类名前面加上“~”符号。
2、没有返回值,并且没有参数。
3、析构函数不能构成函数重载,只能定义一次,如果我们自己没有定义,编译器会自动生成默认的析构函数。
4、对象生命周期结束,自动调用对应的析构函数。

class Time
{
public:
	Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}

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

	void Print()
	{
		//cout << "void Print()" << endl;

		cout << _hour << "时" << _minute << "分" << _second << "秒" << endl;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

	void Print()
	{
		//cout << "void Print()" << endl;

		cout << _year << "年" << _month << "月" << _day << "日" << endl;
		this->t.Print();
	}
private:
	int _year;
	int _month;
	int _day;

	Time t;
};

int main()
{
	//Date d(2023, 3, 14);
	Date d;
	d.Print();

	return 0;
}

析构函数我们不写,编译器会自动生成,那我们什么情况不用写析构,什么情况要写析构呢?
存在栈区的变量,程序结束自动销毁,而堆区,例如malloc开辟出来的空间,如果不及时释放,会造成内存泄漏,所以,有资源申请,就要写析构。

拷贝与赋值

拷贝构造

对于内置类型,我们可以直接赋值,那对象呢?
我们可以在类里面定义一个成员函数,把两个对象传过去(其中一个对象是隐含的this指针),然后依次赋值。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(Date& tmp)
	{
		_year = tmp._year;
		_month = tmp._month;
		_day = tmp._day;
	}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _year;
	int _month;
	int _day;

	//Time t;
};

int main()
{
	Date d(2023, 3, 14);
	d.Print();

	//Date x = d;
	Date x(d);//这种写法和上面这种写法是一样的
	x.Print();

	return 0;
}

这里我们传参数传的是类对象的引用,那可以穿值传参吗?
答案是不可以的,因为我们要先捋清楚,这个拷贝构造函数是把一个对象拷贝给另一个对象,如果是传值调用,以上面代码为例(把参数的&去掉),我们调用这个函数的目的是把d这个类对象拷贝给x对象,如果是传值调用,那么会先创建一个tmp的临时变量,然后会先把d拷贝给tmp,那怎么拷贝给tmp呢?又继续调用这个拷贝构造函数,不断的递归调用,引发无穷递归。
所以,拷贝构造函数的参数只有一个(另一个作为隐含的this指针传递),并且这个参数必须是类对象的引用,另外,如果不用改变原来对象的值(上面代码的d对象),在拷贝构造函数的参数前面最好加上const修饰:

Date(const Date& tmp)
{
	_year = tmp._year;
	_month = tmp._month;
	_day = tmp._day;
}

像这样的日期类对象,拷贝的全部都是值拷贝,在拷贝构造函数中,拷贝方式有两种,一种是浅拷贝,值拷贝就是浅拷贝,还有一种是深拷贝,深拷贝是什么呢?
例如数据结构中的栈队列,实现栈的时候,需要malloc开辟一块空间,而拷贝过程中,传参的参数是指针,是这块空间的地址,把地址拷贝过去,那不就相当于两个对象同时指向同一块空间吗?

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_array = (int*)calloc(capacity, sizeof(int));
		if (_array == nullptr)
		{
			perror("calloc fail");
			exit(-1);
		}
		_sz = 0;
		_capacity = capacity;

	}

	~Stack()
	{
		assert(_array);
		free(_array);
		_array = nullptr;
		_sz = _capacity = 0;
	}

	Stack(const Stack& tmp)
	{
		_array = tmp._array;
		_sz = tmp._sz;
		_capacity = tmp._capacity;
	}

private:
	int* _array;
	int _sz;
	int _capacity;
};

int main()
{
	Stack s1;

	Stack s2 = s1;

	return 0;
}

我们通过调式代码看看:
在这里插入图片描述
上面是我们自己写的默认拷贝构造函数,如果注释掉,编译器就会自动生成一个默认的拷贝构造函数,其效果是一样的(两个对象指向同一块空间)。这里,我们就可以得出:默认生成的拷贝构造函数是浅拷贝。
像这种情况,我们就得自己写一个深拷贝构造函数:

Stack(const Stack& tmp)
	{
		_array = (int*)calloc(tmp._capacity, sizeof(int));
		if (_array == nullptr)
		{
			perror("calloc fail");
			exit(-1);
		}
		memcpy(_array, tmp._array, sizeof(int) * tmp._capacity);
		_sz = tmp._sz;
		_capacity = tmp._capacity;
	}

赋值重载

首先,我们先了解一下运算符重载:
运算符重载是具有特殊函数名的函数,它和普通函数差不多,也具有函数返回值,函数名和参数列表。
函数名:operator后紧跟需要重载的运算符,这个运算符不能是随意加的,像¥#@这样的符号就不行。

另外,也有5个运算符不能构成重载:
1、.* (类成员指针访问运算符)
2、sizeof 计算大小的操作符
3、?: 三目操作符
4、:: 域操作符
5、. 解引用操作符

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	bool operator==(Date& tmp)
	{
		return _year == tmp._year
			&& _month == tmp._month
			&& _day == tmp._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 3, 14);
	Date d2(2023, 3, 14);

	cout << (d1 == d2) << endl;
	return 0;
}

当然,也可以写在类外面,变成全局函数,这样的话就得获取成员变量了,但是成员变量的权限是private,不能直接访问,可以用友元函数,或者在类里面定义一个获取成员变量值的函数。
我们再回到赋值重载
赋值重载函数的定义和上面的运算符重载定义差不多

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

	Date& operator=(const Date& tmp)
	{
		_year = tmp._year;
		_month = tmp._month;
		_day = tmp._day;
		return *this;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 3, 14);
	Date d2;

	d2 = d1;

	d1.Print();
	d2.Print();

	return 0;
}

还可以再加一点提高效率:

Date& operator=(const Date& tmp)
{
	if (this != &tmp)//如果是自己给自己赋值就没必要走进来了。
	{
		_year = tmp._year;
		_month = tmp._month;
		_day = tmp._day;
		return *this;
	}
}

赋值函数如果我们没有写,编译器会自己默认生成,所以,如果我们把上面自己定义的赋值函数注释掉,依然可以用。
但是,如果把它移到全局去(放在类的外面),变成全局函数,那么就会编译不通过,因为在类里面没有定义赋值重载,编译器会自动生成一个,而全局又有一个函数名、参数类型、参数类型顺序、参数个数都相同的函数,就不构成函数重载,所以编译失败。
从而得出,赋值重载必须定义为成员函数。
那既然赋值函数不写编译器会自动生成,那我们还要写吗,或者说是什么情况下要写,什么情况可以不写?

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_array = (int*)calloc(capacity, sizeof(int));
		if (_array == nullptr)
		{
			perror("calloc fail");
			exit(-1);
		}
		_sz = 0;
		_capacity = capacity;

	}

	~Stack()
	{
		assert(_array);
		free(_array);
		_array = nullptr;
		_sz = _capacity = 0;
	}

	Stack(const Stack& tmp)
	{
		_array = (int*)calloc(tmp._capacity, sizeof(int));
		if (_array == nullptr)
		{
			perror("calloc fail");
			exit(-1);
		}
		memcpy(_array, tmp._array, sizeof(int) * tmp._capacity);
		_sz = tmp._sz;
		_capacity = tmp._capacity;
	}

	void Push(int day)
	{
		if (_sz == _capacity)
		{
			int* tmp = (int*)realloc(_array, sizeof(int) * 2 * _capacity);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_array = tmp;
			_capacity *= 2;
		}
		_array[_sz++] = day;
		
	}

	void Print()
	{
		for (int i = 0; i < _sz; ++i)
		{
			cout << _array[i];
			
		}
		cout << endl;
	}

private:
	int* _array;
	int _sz;
	int _capacity;
};

int main()
{
	Stack s1;

	Stack s2;
	s2 = s1;

	s1.Push(1);
	s1.Push(2);
	s1.Push(3);

	

	s2.Push(4);

	s1.Print();
	s2.Print();

	return 0;
}

在这里插入图片描述
结论:当有资源需要释放时,就要写赋值重载,没有就可以不用写。

取地址和const取地址操作符重载

我们先看下面代码:

class Date
{
private:
	int year;
	int month;
	int day;
public:

	void Print()//const
	{
		cout << year << "年" << month << "月" << day << "日" << endl;
	}

	Date(int y = 1, int m = 1, int d = 1)
	{
		year = y;
		month = m;
		day = d;
	}
};

void function(const Date& tmp)
{
	const Date a = tmp;

	a.Print();//编译不通过,error C2662: “void Date::Print(void)”: 不能将“this”指针从“const Date”转换为“Date &”
}

int main()
{
	Date d(2023, 3, 14);

	d.Print();

	function(d);
	return 0;
}

因为a是const Date类型的对象,而Print的参数类型是Date,权限放大,所以不能访问该成员函数,如果要访问,就得改变成员函数的参数类型——this类型,在该函数后面加上const即可,这个const修饰的this指针。
再回过来看取地址和const取地址操作符重载:

Date* operator&()//非const成员两个成员函数都可以用
{
	return this;
}

const Date* operator&()const//而const成员只能用这一个
{
	return this;
}

这两个函数重载一般都用编译器默认生成的,没有特殊需求的话,写不写都行。

总结

构造函数:内置类型得自己写,自定义类型不用写。
析构函数:有资源要清理就写,否则不用。
拷贝构造:要深拷贝就是自己写,浅拷贝用默认生成的。
赋值重载:析构要写,那赋值重载就写,否则就不需要
取地址和const取地址操作符:不需要自己写。

关于构造函数的补充

我们先看一个类:

class A
{
private:
	int a;
	int b;
	const int c;
};

A类中的成员变量c被const修饰了的,但是没有给值,要知道,加了const的变量必须初始化,并且只能给一次,之后不能再修改(具有常属性),那是在什么时候初始化这个c的呢?构造函数里?不对,构造函数里是赋值,那还有另外一种,C++11支持在变量声明时给缺省值,但是在C++11之前呢?
所以,C++还有一个东西叫初始化列表,它是以冒号开始,用逗号分隔的数据成员列表:

class A
{
public:
	A()
		:a(1)
		,b(2)
		,c(3)
	{
		a = 1;
		b = 1;
	}
private:
	int a;
	int b;
	const int c;
};

每个成员变量在初始化时都会经过初始化列表,并且只能出现一次(因为只能初始化一次),其中,像引用成员的变量、const修饰的变量以及自定义类型(没有默认构造函数)这个三个变量必须在初始化列表初始化。
对于其他类型尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
另外,初始化列表里的初始化顺序与列表中先后次序无关,跟变量在声明时的顺序有关。

class Date
{
public:
	Date(int y)
		:year(y)//最后初始化year,year = y
		, month(y)//再初始化month,month = y
		, day(year)//先初始化day,使得day = year,但是year这时还没初始化,所以day是随机值
	{

	}

	void Print()
	{
		cout << year << "年" << month << "月" << day << "日" << endl;
	}
private:
	int day;
	int month;
	int year;

};

int main()
{
	Date d(1);

	d.Print();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值