08【C++ 初阶】类和对象(下篇) --- 类知识的额外补充

发布于:2025-08-04 ⋅ 阅读:(8) ⋅ 点赞:(0)


前言

本文是对前文的补充,旨在更好理解类。


1.再谈构造函数

前文得知构造函数有两个部分组成,一个是初始化列表,一个是函数体。

class Date
{
public:
	Date(int year, int month, int day)
		//:_year(year)   //初始化列表部分
		//,_month(month)
		//,_day(day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

虽然上述构造函数调用之后,确实给每个变量初始化了一个值,但是其实这种在构造函数体中的,不能称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

前文结论:

默认构造函数和初始列表配合,以确保类的所有成员变量(成员对象、内置类变量初始化行为可预测)在进入函数体时之前都已经被初始化了(构造确定性)。

那么有些人可能会疑惑,我们的构造函数函数体同样可以初始化成员(可以在函数体中为所有的内置类成员做初始化、调用自定义类成员的默认构造函数),为什么还需要初始化列表呢?

因为类中有可能有一些初始化之后就无法被修改的,或者说无法做正常赋值“=”的变量,比如:

  1. const修饰的成员变量:一旦初始化就无法修改。
  2. 引用成员变量:一旦初始化为某变量的别名就无法修改。
  3. 没有默认构造函数的自定义类成员对象:只能显示的调用其构造。

以上三种类型的变量需要的其实是初始化,但是一旦走到函数体中,对内置类的操作将是“赋值”,对没有默认构造的对象的操作将无法预测,比如在构造之前访问。

错误例子:

class Day
{
	int _day;
public:
	Day(int day)
		:_day(day)
	{}
};

class Date
{
public:
	Date(int year, int& month, int day)
		//初始化列表会去尝试调用_day的默认构造,没有所以报错: 类"Day"不存在默认构造函数。
	{                     
		_year = year;     //const修饰的变量不可修改,报错: 表达式必须是可修改的左值。
		_month = month;   //这里是普通的赋值,并不是取别名。
		_day(day);        //_day这里其实没有被初始化,编译器默认默认进入函数体的变量是已经初始化过的,所以尝试将它当做函数指针,和 _day 对象的 ​函数调用运算符去调用,
	                      //报错: 在没有适当operator()的情况下调用类类型的对象或将函数转换成指向函数的类型。
	}
private:
	const int _year;
	int& _month;
	Day _day;
};

正确例子:初始化最标准的地方,是初始化列表

class Day
{
	int _day;
public:
	Day(int day)
		:_day(day)
	{
	}
};

class Date
{
public:
	Date(int year, int& month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	const int _year;
	int& _month;
	Day _day;
};
int main()
{
	int a = 2;
	Date d1(1,a,3);
	return 0;
}

所以最终对于默认构造函数和初始化列表的结论:

编译器自动生成的默认构造和构造的初始化列表​保证了​:

  1. 所有自定义类成员在进入构造函数体前已调用其默认构造函数​
  2. 所有内置类型成员完成默认初始化​(即使是没有操作)
  3. 所有const/引用/无默认构造的成员获得有效初始状态​(因为类中可能会有const/引用/无默认构造的成员变量)

总之,默认构造和我们初始化列表的配合,就是为了遵循C++的一个核心原则:构造确定性,即保证了所有类对象的初始化是可预测的。但程序员仍需警惕:内置类型的默认初始化不保证数据安全!​


2.自定义类的隐式类型转换

我们知道,C++的内置类型,允许编译器做隐式类型的转换:

int main()
{
	int a = 1;
	float b = 2;
	double c = 3;

	b = a;      //从int->float

	b = c;      //从float->double

	return 0;
}

这里的float类型,可以从别的类型转换过来,也可以转换成别的类型,所以是两种转换,那么为了语义的一致性,我们的自定义类型,也肯定需要这种转换,而是分为两种:自定义类向内置类的转换,内置类向自定义类的转换。

2.1 内置类->自定义类

C++允许编译器做类型的隐式类型转换。
内置类向自定义类的转换,需要自定义类型的构造函数可以接受单参数去构造对象。---- 类的单参数隐式类型转换。

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

	void print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	int a = 2025;
	Date d;
	d = a;
	d.print();  // 2025 2 3
	d = 2025;
	d.print();  // 2025 2 3
	d = '2025';
	d.print();  // 842019381 2 3
	d = "2025";
	d.print();  // 二元“=”: 没有找到接受“const char [5]”类型的右操作数的运算符(或没有可接受的转换)
	return 0;
}

当编译器识别到有内置类型的变量向自定类型的变量赋值,那么编译器就会自动的去调用这个自定义类型的构造函数,并把这个内置类型的变量想尽办法去向我们的构造函数的接收参数去做隐式类型的转换,然后将它作为构造的参数传入,去构造出一个临时的类对象,然后走正常的operator=的逻辑,将临时的类对象的值,赋值给对象。

注意:

  1. 内置类向自定义类转换只要构造函数可以正常接受的单参数即可,其他的参数有缺省值也可以做隐式类型转换。
  2. 如果是内置类型实在无法转换成自定义类构造函数的参数类型,编译器就会试图去调用operator=,如果没有实现,就会报错。

2.2 自定义类->内置类

C++允许编译器做类型的隐式类型转换。
自定义类向内置类的转换,需要自定义类型有实现对应的类型转换运算符函数。---- 通过类型转换运算符转换。

我们知道类型还可以手动的转换:

int main()
{
	float a = 1.1;
	int b = int(a) + 1;   //将float类型,强制转换成了int类型。
	cout << a << " " << b;   //1.1 2
	return 0;
}

内置类型的强制转换语法(如int()/double())是一种类型转换运算符。
所以我们的自定义类,当然需要可以通过operator去重载这种转换的运算符:

class Date
{
public:
	Date(int year = 1, int month = 2, int day = 3)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	operator int()
	{
		return _year;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;
	int year = d1;
	cout << year;   //1
	return 0;
}

重载类型转换运算符,不允许使用参数,也不允许写返回值,返回值就是我们对应重载的类型转换运算符。

那么:

  1. 我们写了对应类型的转换运算符。
  2. C++又允许编译器去做隐式的类型转换。

所以只要有我们自定义类向内置类型的赋值,编译器就会自动的调用对于类型的转换,隐式的将我们的自定义类转换通过函数的逻辑,转换成对应的内置类型返回,然后就走我们正常的内置类型变量的赋值逻辑了。

2.3 禁用隐式类型转换

以上两种自定义类型的转换方式,编译器都可以通过它们做隐式类型的转换,但是有时候虽然我们有构造函数,也有类型转换运算符重载,但是我们并不想让编译器做自动的隐式转换,我们只想手动的去调用它转换,就可以通过explicit关键字,去禁用它们的隐式类型转换:

class Date
{
public:
	// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用,而且编译器可以通过它做隐式类型的转换.
	Date(int year, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	operator int()
	{
		return _year;
	}

	// 2. explicit修饰函数,禁止类型转换
	/*explicit Date(int year, int month = 1, int day = 1)
	: _year(year)
	, _month(month)
	, _day(day)
	{}
	explicit operator int()
	{
		return _year;
	}*/

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	int a = 2063;
	Date d1(2022);
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
	d1 = 2023;
	a = d1;
	// 将1屏蔽掉,2放开时则以上两行代码编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
	// 无论有没有explicit,我们都可以手动的做转换:
	d1 = Date(2023);
	a = int(d1);

	return 0;
}

3.类的前、后置++运算符重载

我们补充两个比较重要的运算符重载:
前置++和后置++。

对于int类型,我们可以使用单目运算符:++,去实现它的自增:

int main()
{
	int a = 0;
	cout << a << endl;
	cout << a++ << endl;
	cout << ++a << endl;
	cout << a << endl;

	return 0;
}

那么为了深化类的抽象,使类的使用更贴近内置类型的使用,我们也可以通过对运算符++重载,去对实现类的自增逻辑:
根据我们内置类的++规则我们知道,有分为前置和后置的++:

class Num
{
public:
	Num(int num)
		: _num(num)
	{}
	Num& operator++()
	{
		_num++;            //将成员变量++
		return *this;      //返回本对象,因为前置++表达式是修改之后的值.
	}
	Num operator++(int)
	{
		Num tmp(_num);     //构造一个临时变量,存储未++之前的值.
		_num++;            //将成员变量++
		return tmp;        //返回构造的未++的对象,因为后置++表达式是修改之前的值.
	}
	void print()
	{
		cout << _num << endl;
	}
private:
	int _num;
};
int main()
{
	Num a(3);
	(a++).print();
	(++a).print();
	return 0;
}

4.static成员

声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。

4.1 static静态成员函数:

class A
{
private:
	static void privateGetAHello()
	{
		cout << "你好世界" << endl;
	}
public:

	static void publicGetAHello()
	{
		cout << "你好世界" << endl;
	}

};
int main()
{
	A::publicGetAHello();     //你好世界
	//A::privateGetAHello();    //“A::privateGetAHello”: 无法访问 private 成员(在“A”类中声明)
	return 0;
}

它可以不创建类对象,通过类域就可直接访问,但是注意静态成员也需要遵循访问权限限定符。

为什么静态成员函数不需要通过对象访问:

我们知道我们平常使用的类成员函数,它需要通过编译器隐式的传入对象的this指针,因为它可能需要通过this修改类对象的成员变量;
我们static修饰的静态成员函数,他虽然也和普通的成员函数一样放在代码段中,但是其实编译器不对它自动传入this指针,所以它其实和普通的函数一样,只不过是所属于类的域中的,所以我们就可以直接通过域访问。

也正是因为它不传入this指针,所以它只能访问:

类的 static 成员变量(这些变量存在于全局/静态存储区,与对象无关)。
类的 static 成员函数。
传给它的参数。
函数内部的局部变量。
其他全局变量或函数(如果可见)。

编译器对于static静态成员函数的行为:

  1. 编译器看到 static 关键字后,就知道:不需要给这个函数隐式传入 this 指针参数。
    因此,在语法上,它允许你直接使用类名和域解析运算符 :: 来调用它,而无需创建对象(如 MyClass::staticFunc())。
  2. 在编译生成的代码层面,static 成员函数的处理完全等同于一个顶层的普通函数,除了它的名字被修改(Name Mangling)以包含类作用域信息。---- static修饰的成员函数和我们普通的函数一样,只不过它所属于一个类的域(类域之中有使用权)。

static 成员函数就是一个被限定在类作用域里的普通函数(遵循类的访问权限限定符)。它被“附加”给类(只是所属于该类域),主要是因为它的逻辑与类密切相关(比如操作 static 数据,或者作为工厂方法),但它并不操作任何具体的类对象实例(因为不隐式的传this指针)。

4.2 static静态成员变量:

class A
{
public:
	static int _public_count;

private:
	static int _private_count;
};
int A::_public_count = 0;
int A::_private_count = 0;   //虽然private的静态成员确实不可以在类外访问,但是必须它必须要定义,所以特例在全局位置可以给它初始化.
int  a = A::_private_count;   //如果是访问就不行:“A::_private_count”: 无法访问 private 成员(在“A”类中声明)


int main()
{
	cout << A::_public_count << endl;      //0
	//cout << A::_private_count << endl;   //“A::_private_count”: 无法访问 private 成员(在“A”类中声明)
	return 0;
}

和静态成员函数一样,访问不用创建一个类对象,通过类域就可直接访问,但是注意静态成员也需要遵循访问权限限定符。

为什么静态成员变量不需要通过对象访问:

像我们平常的成员变量,它是我们类对象的一部分,存在栈、堆、全局数据区的内存空间之中的,如果我们的成员函数需要访问或者操作这个对象,它需要得知这个对象的内存地址(this指针);
而我们的静态成员变量,它并不存在每一个对象中,而是存在进程的全局数据区,所以它其实和普通的全局变量一样,只不过存在类的域中,所以我们就可以直接通过域访问。

我们知道,平时定义一个类,只是对它里面的变量做声明,所以静态变量,需要我们在类外去额外的定义(即分配内存,即初始化)。
编译器对于static静态成员变量的行为:

编译器看到 static 关键字后,就知道:​​

  1. 这个变量不属于类的任何单个对象实例。
    它需要在程序的全局数据区(.data 或 .bss 段)​​ 分配内存,拥有静态存储期​(在 main 之前创建,main 之后销毁)。
    整个程序中,该变量只有唯一的一份内存副本,被该类的所有对象实例共享。
  2. 在编译和链接层面:​​
    类定义内部的 static 成员变量声明(static int staticVal;)​仅是一个声明,它不分配实际存储空间。它告诉编译器变量的类型、名字和作用域。
    编译器要求必须在一个且仅一个源文件(.cpp)​​ 中,使用类名和作用域解析运算符 :: ​对该变量进行定义(并可选初始化)​​(如 int MyClass::staticVar = 0;)。
    这个在 .cpp 文件中的定义语句,才是真正触发编译器/链接器在全局数据区为该变量分配存储空间的地方。编译器/链接器确保整个程序只有这一个定义(只分配一次内存)。
    它的名字在链接时也会经过名字修饰(Name Mangling)​,包含类作用域信息(如 _ZN7MyClass10staticVarE),以区分不同类中同名的静态成员变量或全局变量。

静态成员变量就是一个被限定在类作用域里的全局变量(遵循类的访问权限限定符)。它被“附加”给类(只是所属于该类域),主要是因为它的数据逻辑与类本身密切相关(比如作为所有对象共享的计数器或配置)。它不与任何具体的类对象实例相关联(因为不需要 this 指针定位),整个程序只有一份存储在全局数据区的副本,生命周期贯穿整个程序运行期。访问它需要通过类名限定(或对象,但本质相同),并且必须在类外单独定义和初始化。


5.友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类

5.1 友元函数

我们知道,C++的标准输出流cout的使用方法是:

#include<iostream>
using namespace std;
int main()
{
	int a = 666;
	cout << "hello world!" << endl;   //hello world!
	cout << a << endl;                //666

	return 0;
}

为什么cout直接加上“<<”就可以将后面的内容、变量,输出到字符串呢?这个“<<”,到底是什么呢?

其实我们的cout,就是我们STL标准模板库中的一个类std::ostream的全局对象,而“<<”就是该类的一个运算符重载而已!

所以其实我们“<<”后面接的,都是作为“<<”运算符符重载函数的参数,而我们的cout可以打印各种内置类型的参数,证明它已经被前人重载了多次,让各种类型都有对应的函数重载:

//截取自ostream类内的成员函数定义:
    basic_ostream& __CLR_OR_THIS_CALL operator<<(int _Val) { // insert an int
        //...
    }

    basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned int _Val) { // insert an unsigned int
		//...
    }
    basic_ostream& __CLR_OR_THIS_CALL operator<<(long _Val) { // insert a long
        //...
    }

    basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned long _Val) { // insert an unsigned long
        //...
    }

    basic_ostream& __CLR_OR_THIS_CALL operator<<(long long _Val) { // insert a long long
        //...
    }

可见operator<<将所有常见的内置类型都重载了,
那么我们尝试一下,让cout,可以打印我们自己实现的类:

尝试在自己的类中实现operator<<?

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{
	}
	// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
	// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

可见,我们自己的类中,第一参数,永远是编译器隐藏添加的this指针,是本类的指针,所以我们对operator的调用也应当是自己的类在前面,如:MyClassObject << cout;,这样显然是错误的,

其实,我们的operator运算符重载,它不仅可以写在类域中,还可以作为一个普通的函数,实现在各种域中,根据编译器的函数查找规则,可以被编译器匹配到:
不过我们的运算符重载还是需要遵循函数重载的规则,即:

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

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

ostream& operator<<(ostream& _cout, const Date d)
{
	_cout << "666你真是个大神,打印了一个日期: year-" << d._year << " month-" <<d._month << " day-" << d._day << endl;  //“Date::_year”: 无法访问 private 成员(在“Date”类中声明)
	return _cout;
}

int main()
{
	Date d(2025, 8, 3);
	cout << d << endl;
	return 0;
}

可是作为Date类中私有的成员变量,如何被我们外部访问呢?难道将成员放为公有?这样不就破坏了我们类的封装性吗?!

所以,我们需要在保证类大部分封装性的前提下,将访问权开放给特定的一些函数,这个特殊的,可以访问我们类中私有成员的函数,就叫做该类的友元函数(朋友函数,即只有你这个挚友,才可以访问到我内心深处的秘密啊!)。

友元函数的声明方法:即在类中使用friend关键字,对该函数进行声明。

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date d);
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

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

ostream& operator<<(ostream& _cout, const Date d)
{
	_cout << "666你真是个大神,打印了一个日期: year-" << d._year << " month-" <<d._month << " day-" << d._day << endl;
	return _cout;
}

int main()
{
	Date d(2025, 8, 3);
	cout << d << endl;
	return 0;
}

对于cin同理:

class Date
{
	friend istream& operator>>(istream& _cin, Date& d);
	friend ostream& operator<<(ostream& _cout, const Date d);
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& _cout, const Date d)
{
	_cout << "666你真是个大神,打印了一个日期: year-" << d._year << " month-" <<d._month << " day-" << d._day << endl;  //“Date::_year”: 无法访问 private 成员(在“Date”类中声明)
	return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}
int main()
{
	Date d(2025, 8, 3);
	cout << d << endl;    //666你真是个大神,打印了一个日期: year-2025 month-8 day-3
	cin >> d;             //输入1234 5 6;
	cout << d << endl;    //666你真是个大神,打印了一个日期: year-1234 month-5 day-6
	return 0;
}

友元函数,即在类的内部使用friend关键字(朋友字段)声明的外部的函数,使外部的函数,可以访问类内的私有成员。

5.2 友元类

和友元函数同理,一个类中声明的友元类,即是它的朋友,可以访问它的成员。
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

注意:

  • 友元关系是单向的,不具有交换性,即你是我的朋友,但是我在你心中未必是你的朋友。
    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time
    类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递,你是我的朋友,你的朋友未必是我的朋友。
    如果B是A的友元,C是B的友元,则不能说明C时A的友元。
  • 友元关系不能继承,在继承位置再给大家详细介绍。
class Time
{
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour), _minute(minute)
		, _second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d;
	d.SetTimeOfDate(666, 666, 666);
	return 0;
}

友元类,即在类A的内部使用friend关键字(朋友字段)声明的其他类B,使类B的所有函数,都可以访问类A内的私有成员。


6.内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK,但是是随机值哦.
		}
	};
};
int A::k = 1;
int main()
{
	A::B b;
	A a;
	b.foo(a);

	return 0;
}

内部类是定义在外部类中的类,内部类一定是外部类的友元类,遵循友元类的规则。


7.再理解类和对象

面向对象是一种编程思想,其核心在于将复杂的系统分解为一组相互协作的对象(整体)​。这些对象责任明确、边界清晰,模拟现实世界中的实体及其交互。这取代了传统基于全局数据和零散函数调用的方式,使程序更贴近现实,结构更清晰,耦合度更低。

在这个思想指导下,​类(Class)​​ 是 C++ 实现面向对象的直接产物和关键工具。​类本质上是一种由程序员自定义的类型。它允许我们将现实世界中某一类实体的属性和行为集中在一起进行描述。

这种集中描述的核心目标是实现面向对象的两个基石:

  1. 封装​​:这是实现面向对象的基础。类的封装性主要体现在将描述一个实体所需的数据(属性)​​ 和操作这些数据的函数(方法)​绑定在一起。例如,描述一个洗衣机类时,它的“容量”、“当前状态”等数据成员,以及“启动洗涤”、“甩干”等成员函数,都被统一封装在 WashingMachine 类内部。
  2. ​ 抽象​​:类的抽象性是指它仅公开必要的操作接口(成员函数),而隐藏其内部实现细节(数据和实现逻辑)​。使用者只需知道 如何使用 这个类(比如调用 startCycle() 方法),而无需关心洗衣机内部具体如何注水、加热、转动。这使类成为一个易于理解和使用的整体单元。

为了精确实现这种封装和抽象,C++ 提供了访问限定符​(public、private、protected)。开发者可以通过它们控制类成员的可见性:

  • 将类内部的复杂实现细节(数据和具体实现方法)声明为 private 或 protected(隐藏细节)。
  • 将外部需要使用的关键功能接口声明为 public(公开接口)。
    这种访问控制是降低对象之间耦合度、实现抽象的关键机制。

洗衣机例子:​​

  1. 识别与抽象(设计层面):​​ 我们先分析现实中的洗衣机实体,确定它作为对象的关键属性​(如型号、容量、当前状态)和核心功能​(如启动洗涤、漂洗、甩干)。
  2. 定义类(实现层面):​​ 用 C++ 定义一个 WashingMachine ​类。在类定义中:
  • 封装​:将属性声明为成员变量(model, capacity, state),将与洗衣机操作相关的行为声明为成员函数(startCycle(), rinse(), spin())。
  • ​抽象​:使用访问限定符(如 public: 对外公开 startCycle() 等方法,private: 隐藏内部状态 state 和具体实现逻辑)。
    ​3. 实例化对象(使用层面):​​ 代码中通过 WashingMachine myWasher; 语句,使用该 ​自定义类型 (WashingMachine)​​ ​实例化出具体的洗衣机对象​ myWasher。这个对象封装了特定洗衣机的状态和行为。
  1. 对象协作(运行层面):​​ 程序通过操作该对象(如调用 myWasher.startCycle();)来模拟现实洗衣机的运作。使用者只需要与公开的接口交互,无需了解其内部复杂的实现细节。

在面向对象编程中,​类是我们为现实世界中某一类实体创建自定义类型的核心工具。通过封装,它将描述实体所需的数据和行为组织在一起;通过抽象​(借助访问控制),它公开简洁的接口并隐藏复杂的实现细节,使对象成为可以独立存在和相互协作的“整体”。理解了类的这种封装和抽象机制,就掌握了面向对象程序设计中如何建模和组织代码的关键。


全文总结

  1. 默认构造函数和初始化列表的最终结论:

    编译器自动生成的默认构造和构造的初始化列表​保证了​:

    1. 所有自定义类成员在进入构造函数体前已调用其默认构造函数​
    2. 所有内置类型成员完成默认初始化​(即使是没有操作)
    3. 所有const/引用/无默认构造的成员获得有效初始状态​(因为类中可能会有const/引用/无默认构造的成员变量)

    总之,默认构造和我们初始化列表的配合,就是为了遵循C++的一个核心原则:构造确定性,即保证了所有类对象的初始化是可预测的。但程序员仍需警惕:内置类型的默认初始化不保证数据安全!

  2. 隐式类型转换:
    内置类可以类型转换,所以也得想办法让自定义类可以:

    • 自定义类向内置类转换:

    当编译器识别到有内置类型的变量向自定类型的变量赋值,那么编译器就会自动的去调用这个自定义类型的构造函数,并把这个内置类型的变量想尽办法去向我们的构造函数的接收参数去做隐式类型的转换,然后将它作为构造的参数传入,去构造出一个临时的类对象,然后走正常的operator=的逻辑,将临时的类对象的值,赋值给对象。

    • 内置类向自定义类转换:
    1. 我们写了对应类型的转换运算符。
    2. C++又允许编译器去做隐式的类型转换。

    所以只要有我们自定义类向内置类型的赋值,编译器就会自动的调用对于类型的转换,隐式的将我们的自定义类转换通过函数的逻辑,转换成对应的内置类型返回,然后就走我们正常的内置类型变量的赋值逻辑了。

  3. 类前、后置++运算符重载:
    对自增运算符++的重载,分为前置++和后置++,为了区分,后置++多带一个的int参数,只为区分不做用处。

  4. static成员

    • static静态成员函数:
      编译器对于static静态成员函数的行为:
    1. 编译器看到 static 关键字后,就知道:不需要给这个函数隐式传入 this 指针参数。
      因此,在语法上,它允许你直接使用类名和域解析运算符 :: 来调用它,而无需创建对象(如 MyClass::staticFunc())。
    2. 在编译生成的代码层面,static 成员函数的处理完全等同于一个顶层的普通函数,除了它的名字被修改(Name Mangling)以包含类作用域信息。---- static修饰的成员函数和我们普通的函数一样,只不过它所属于一个类的域(类域之中有使用权)。

    static 成员函数就是一个被限定在类作用域里的普通函数(遵循类的访问权限限定符)。它被“附加”给类(只是所属于该类域),主要是因为它的逻辑与类密切相关(比如操作 static 数据,或者作为工厂方法),但它并不操作任何具体的类对象实例(因为不隐式的传this指针)。

    • static静态成员变量:
      编译器对于static静态成员变量的行为:

    编译器看到 static 关键字后,就知道:​​

    1. 这个变量不属于类的任何单个对象实例。
      它需要在程序的全局数据区(.data 或 .bss 段)​​ 分配内存,拥有静态存储期​(在 main 之前创建,main 之后销毁)。
      整个程序中,该变量只有唯一的一份内存副本,被该类的所有对象实例共享。
    2. 在编译和链接层面:​​
      类定义内部的 static 成员变量声明(static int staticVal;)​仅是一个声明,它不分配实际存储空间。它告诉编译器变量的类型、名字和作用域。
      编译器要求必须在一个且仅一个源文件(.cpp)​​ 中,使用类名和作用域解析运算符 :: ​对该变量进行定义(并可选初始化)​​(如 int MyClass::staticVar = 0;)。
      这个在 .cpp 文件中的定义语句,才是真正触发编译器/链接器在全局数据区为该变量分配存储空间的地方。编译器/链接器确保整个程序只有这一个定义(只分配一次内存)。
      它的名字在链接时也会经过名字修饰(Name Mangling)​,包含类作用域信息(如 _ZN7MyClass10staticVarE),以区分不同类中同名的静态成员变量或全局变量。

    静态成员变量就是一个被限定在类作用域里的全局变量(遵循类的访问权限限定符)。它被“附加”给类(只是所属于该类域),主要是因为它的数据逻辑与类本身密切相关(比如作为所有对象共享的计数器或配置)。它不与任何具体的类对象实例相关联(因为不需要 this 指针定位),整个程序只有一份存储在全局数据区的副本,生命周期贯穿整个程序运行期。访问它需要通过类名限定(或对象,但本质相同),并且必须在类外单独定义和初始化。

  5. 友元

    • 友元函数

    友元函数,即在类的内部使用friend关键字(朋友字段)声明的外部的函数,使外部的函数,可以访问类内的私有成员。

    • 友元类

    友元类,即在类A的内部使用friend关键字(朋友字段)声明的其他类B,使类B的所有函数,都可以访问类A内的私有成员。

  6. 内部类

    内部类是定义在外部类中的类,内部类一定是外部类的友元类,遵循友元类的规则。

  7. 面向对象再理解

    在面向对象编程中,​类是我们为现实世界中某一类实体创建自定义类型的核心工具。通过封装,它将描述实体所需的数据和行为组织在一起;通过抽象​(借助访问控制),它公开简洁的接口并隐藏复杂的实现细节,使对象成为可以独立存在和相互协作的“整体”。理解了类的这种封装和抽象机制,就掌握了面向对象程序设计中如何建模和组织代码的关键。


本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点:)。


网站公告

今日签到

点亮在社区的每一天
去签到