C++中的类

1、类和对象

C++中通过class定义类

class A
{
   int a;
};  // 定义一个A类型的类

通过类来定义对象

A a; // 定义一个A类型的对象

类是一张蓝图,是抽象的。而对象是根据蓝图真正建造出来的建筑,是具象的。

对象是类的实体化

2、类的限制修饰符

类有三种修饰符:public、private和protected

public表示公有,即类内外都可以访问

private表示私有,即只能类内成员访问

protected表示受保护的,也是只能类内成员访问

从简单理解类的角度来看,可以认为后两者是一样的,他们的区别主要在于继承

class Person
{
public:
   void PrintPersonInfo()
   {
      for (int i = 0; i < 20; ++i)
          printf("%c ", _name[i]);
      printf("\nage: %d", _age);
   }
private:
   char _name[20];
   char _gender[3];
   int _age;
};

在类中,类内成员函数是可以访问类内成员变量的,访问限定符是限制类外对类内的访问

访问限定符只在编译期有效,真正代码映射到内存后,是没有用的

3、计算类的大小

计算类的大小就需要考虑两个因素:成员函数和成员变量,因为类中就只有这俩哥们

但是成员函数是不占用空间的,这是因为C++中类成员函数的存储方式是:所有成员函数均存放在公共代码区,在编译期间就确定了这些成员函数的地址

对于成员函数的存储方式,要注意的是:

1. 成员函数是属于整个类的,和一个具体的对象无关

2. 计算类的大小不包含成员函数,只需要考虑成员变量即可

因此计算类的大小就和C语言中计算结构体大小类似

比如说这个类

class A
{
public:
    void print();
    int _a;
    char _c;
}

根据结构体对齐规则,就可以知道大小应该是12字节

结构体对齐规则:

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
    所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

理解成员函数的存储方式

class A
{
public:
    void print()
    {
        return;
    }

};

int main() {
    A* a = nullptr;
    a->print();
    cout << "a->print() execute" << endl;
    return 0;
}

代码运行结果
a->print() execute

这段代码之所以能够正常运行,是因为函数的地址和对象是无关的,函数的地址在编译期间便已经确定,因此当执行a->print()的时候,并不需要对nullptr解引用,而是直接call 对应的成员函数地址

空类和空类所定义的对象占用1字节空间,用来标识对象以及类是存在的(空类指没有成员变量的类)

4、this指针

this指针是对象的地址,用来标识一个对象

由于成员函数是所有对象共享的,而成员函数很多时候需要去访问成员变量(不访问成员变量放类里面干啥)

class A
{
public:
    void print()
    {
        cout << _a << endl;
    }
    int _a;

};

int main() {
    A a1;
    cout << a1._a << endl;
    return 0;
}

比如这里a1去访问print(),而print是需要查看a1的_a的,这个_a是存在于a1对象中的,因此成员函数必须要有a1对象的地址才能够去访问到_a变量。

由此可以看出,成员函数,要具有访问任意一个对象的能力,因此就需要this指针

成员函数其实有一个隐藏的参数,就是this指针,以上面的class A中的print为例,其实它的形式是:

void print(const A* this)
{
    cout << this._a << endl;
}

每次a1调用成员函数,都会将自己的地址传递给this,因此成员函数就具有了访问对象的能力

5、几种特殊的成员函数

5.1、构造函数

构造函数只负责对象的初始化,并不负责对象空间的开辟

构造函数的名字和类名相同,无返回值,参数自定义

如果我们不自己写构造函数,编译器才会默认生成一个构造函数,这个构造函数对内置类型(int/double/char等)不做处理,对自定义类型去调用它的构造函数

构造函数支持重载和缺省参数

默认构造函数指不需要传参数就能够调用的构造函数

默认构造函数包括编译器生成的、无参构造和全缺省构造,至少要包含一个默认构造函数,当然,第一个默认构造不可能与后两者共存,后两者也不建议同时写,因为调用会出歧义。

class A
{
public:

    int _a;
    int _b;
    int _c;
};

对于这个类来说,下面的都是默认构造函数

    A() {}  // 无参
    A(int a = 1) {}  // 全缺省
    A(int b = 1) {}  // 全缺省
    A(int c = 1) {}   // 全缺省
    A(int a = 1, int b = 2) {}  // 全缺省
    A(int a = 2, int c = 1) {}  // 全缺省
    A(int b = 2, int c = 1) {}  // 全缺省
C++11补丁

构造函数按理来说不应该不处理内置类型的参数,所以c++11打了一个补丁,支持在成员变量声明的时候给予缺省值(不是赋值)

class A
{
public:

    int _a = 1;
    int _b = 2;
    int _c = 3;
};

5.2、析构函数

析构函数并不是完成对象本身的清理工作,而是完成对象中资源的清理

析构函数名字为 ~类名,无参数,无返回值,析构函数不支持重载

我们不写析构函数,编译器会给我们默认生成一个析构函数,这个析构函数对于自定义类型调用其析构函数,内置类型不做处理(因为内置类型自己会销毁)

这里的建议是,如果类中申请过资源,对象销毁后,资源仍还在(堆上),那么就需要自己写析构函数对齐进行清理

5.3、拷贝构造函数

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

  1. 拷贝构造函数是构造函数的一个重载形式,所以,拷贝构造也是构造函数
  2. 拷贝构造的形参只有一个,且必须是引用,否则会引发无穷递归

以下面的代码来解释一下为什么会引发无穷递归

class Date
{
public:
    Date(const Date date)
    {
        ...
    }
};

int main()
{
    Date date;
    Date b(date);
    return 0;

这里的A b(a)语句将a传给一个Date类型的形参,需要进行拷贝构造来构造出a,拷贝构造需要传参,传参又是拷贝构造,就成了死结

在这里插入图片描述

我们不写,编译器会默认生成一个拷贝构造函数,这个拷贝构造函数执行的是浅拷贝

深浅拷贝的问题

深拷贝:对于指针等资源,不是单纯的拷贝值,需要为拷贝重新申请一份资源

浅拷贝:仅拷贝值,又称值拷贝

查看下面这段代码

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

这个类中,在堆上申请了一个数组,由于执行的是默认拷贝构造,会把s1中的 _array 的值拷贝给s2,由于析构函数会free(_array), 所以_array就会被free两次,程序会崩溃

所以对于这种,还是得用深拷贝,为s2单独开辟资源

因此,如果需要深拷贝,那么就需要我们自定义拷贝构造,否则不需要

5.4、运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

注意:运算符重载不一定是类的成员函数,它可以声明定义在类外,和类是两个独立个体

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

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

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • . * :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

这里通过一个日期类来阐述运算符重载的使用

class Date
{
public:
	// 全缺省默认构造
	Date(int year = 2023, int month = 7, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};

// 支持运算符重载
bool operator==(const Date&d1, const Date& d2)
{
	return (d1._year == d2._year) &&
		(d1._month == d2._month) &&
		(d1._day == d2._day);
}

int main()
{
	Date d1;
	Date d2;
	cout << (d1 == d2) << endl;  // d1传递给第1个形参,d2传递给第2个形参
	return 0;
}

这里把运算符重载写到了类外面,同样也可以写在类内,写在类内,就会多一个隐藏的this指针的形参,所以就只需要额外写一个形参即可

写了一段完整的日期类代码,如下示

#include <iostream>
#include <cstdio>


using namespace std;

int monthToDay[] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

class Date
{
public:
	// 全缺省默认构造
	Date(int year = 2023, int month = 7, int day = 4)
	{
                // 检验日期是否合法
		if ((IsLeapYear(year) && month == 2 && day > 29)
			|| (!IsLeapYear(year) && month == 2 && day > 28)
			|| ((month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) && day > 31)
			|| ((month == 2 || month == 4 || month == 6 || month == 9 || month == 11) && day > 30)
			|| year < 1 || month < 0 || day < 0 || month > 12)
		{
			cout << "非法日期!!!" << endl;
			exit(-1);
		}

		_year = year;
		_month = month;
		_day = day;
	}

	int GetYear() const
	{
		return _year;
	}

	int GetMonth() const
	{
		return _month;
	}

	int GetDay() const
	{
		return _day;
	}

	// 判断是否是闰年
	bool IsLeapYear(int year) const
	{
		return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
	}



	// 支持运算符重载
	bool operator==(const Date& d2) const
	{
		return ((_year == d2.GetYear()) &&
			(_month == d2.GetMonth()) &&
			(_day == d2.GetDay()));
	}

	bool operator!=(const Date& d2) const
	{
		return !((*this) == d2);
	}

	void operator=(const Date& d)
	{
		_year = d.GetYear();
		_month = d.GetMonth();
		_day = d.GetDay();
	}

	// 加上一个天数
	Date operator+(int newDay) const
	{
		// 计算年月日
		int day = _day + newDay;
		int month = _month;
		int year = _year;
		// 先判断年,再判断月
		if (IsLeapYear(year)) monthToDay[2] = 29;
		else monthToDay[2] = 28;
		while (day > monthToDay[month])
		{
			day -= monthToDay[month];
			++month;
			if (month > 12)
			{
				month -= 12;
				++year;
				if (IsLeapYear(year)) monthToDay[2] = 29;
				else monthToDay[2] = 28;
			}
		}
		Date d(year, month, day);
		return d;
	}
	// -
	Date operator-(int newDay) const
	{
		int day = _day;
		int month = _month;
		int year = _year;
		if (IsLeapYear(year)) monthToDay[2] = 29;
		else monthToDay[2] = 28;
		while (day < newDay)
		{
			newDay -= day;
			--month;
			if (month < 1)
			{
				--year;
				if (year < 0)
				{
					cout << "运算非法!!" << endl;
					exit(1);
				}
				month = 12;
				if (IsLeapYear(year)) monthToDay[2] = 29;
				else monthToDay[2] = 28;
			}
			day = monthToDay[month];
		}

		day -= newDay;

		Date d(year, month, day);
		return d;
	}
	// +=
	Date& operator+=(int newDay)
	{
		(*this) = (*this) + newDay;
		return (*this);
	}
	// 
	// -=
	Date& operator-=(int newDay)
	{
		(*this) = (*this) - newDay;
		return (*this);
	}
	// >
	bool operator>(const Date& d1) const
	{
		if (_year > d1.GetYear()) return true;
		else if (_year < d1.GetYear()) return false;
		else
		{
			if (_month > d1.GetMonth()) return true;
			else if (_month < d1.GetMonth()) return false;
			else
			{
				if (_day > d1.GetDay()) return true;
				else return false;
			}
		}
	}
	// <

	bool operator<(const Date& d1)
	{
		return (d1 > (*this)) && (d1 != (*this));
	}
private:
	int _year;
	int _month;
	int _day;
};



int main()
{
	Date d1;
	Date d2;
	d1 += 1000;
	printf("%d : %d : %d\n", d1.GetYear(), d1.GetMonth(), d1.GetDay());
	return 0;
}

测试日期类是否正确: https://time.org.cn/riqi/

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值