【c++】类和对象(下)

目录

  • 1. 初始化列表的妙用
    • 1.1 为什么需要初始化列表
    • 1.2 初始化列表的优势
    • 1.3 示例
    • 1.4 注意
  • 2.明确禁止隐式转换:explicit关键字
    • 2.1隐式类型转化
    • 2.2 explicit关键字的作用
    • 2.3 使用场景
  • 3.静态成员的独特魅力
    • 3.1 静态成员变量
    • 3.2 静态成员函数
  • 4. 匿名对象:一次性的便利
    • 4.1 匿名对象的定义和使用
    • 4.2 使用场景
  • 5. 友元关系的深入理解
    • 5.1 友元函数
    • 5.2 友元类
  • 6. 内部类:隐藏的艺术
    • 6.1 内部类的定义
  • 7. 编译器优化:背后的智能
    • 7.1参数优化
    • 7.2返回值优化

在C++的世界里,类和对象构成了程序设计的核心。本文旨在深入探讨C++中关于类和对象的高级特性,通过具体示例和详细解释,我们将一探究竟这些特性的强大之处。

1. 初始化列表的妙用

1.1 为什么需要初始化列表

回顾一下之前的常规初始化方式

class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1)
    {
    	//赋值的方式进行“初始化”
    	_year = year;
    	_month = month;
    	_day = day;
    }
private:
    int _year;//此处仅仅是变量的声明,而非定义
    int _month;
    int _day;
};

这种“初始化”的方式严格来讲并不算初始化,只是使用了赋值的形式对已经初始化的变量进行赋值。因此变量在被赋值之前应该已经初始化完成了(这个初始化的过程是编译器给变量置以随机值实现初始化)。

这个过程对int类型好像没什么影响。但对const成员变量引用类型就会出现问题。

class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1,const int a = 10)
    {
    	//赋值的方式进行“初始化”
    	_year = year;
    	_month = month;
    	_day = day;
    	_a = a;//此处会产生报错
    }
private:
    int _year;//此处仅仅是变量的声明,而非定义
    int _month;
    int _day;
    const int _a;
};

对于const成员变量和引用类型,他们只能初始化一次,因为他们需要在定义的地方就需要完成初始化。而这种赋值相当于在其初始化之后进行二次赋值,当然会发生报错。

因此,初始化列表就诞生了。
在C++中,初始化列表提供了一种在构造函数体执行之前初始化成员变量的方式他是类对象的所有成员变量定义及初始化的位置。这对于const成员变量引用类型尤为重要,因为它们一旦被创建就必须被初始化。初始化列表确保了这些变量在对象生命周期开始时就已经被正确初始化。

1.2 初始化列表的优势

效率更高:与传统的赋值初始化相比,初始化列表直接初始化成员变量,减少了一次赋值操作。
必要性:对于const成员变量和引用类型,初始化列表不是选择,而是必须因为这些类型的成员变量不能在构造函数体内被赋值。

1.3 示例

初始化列表基本形式:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

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

1.4 注意

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时)
#include 
#include 
#include 

using namespace std;

class Time
{
public:
	Time(int hour = 0)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
		int _hour;
};
class Date
{
public:


private:
	int _day = 1;
	Time _t;
};
int main()
{
	Date d;
}

见上面代码,即便没有写构造函数,也会在系统生成的默认构造函数中完成初始化,即day使用缺省值1(缺省值会在初始化列表中完成初始化),_t对象调用他的默认构造函数来完成初始化

所以,尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,
一定会先使用初始化列表初始化

  1. 成员变量在初始化列表中的初始化顺序是其在类中声明次序与其在初始化列表中的先后
    次序无关
class A
{
public:
  A(int a)
   :_a1(a)
   ,_a2(_a1)
 {}
 
  void Print() {
    cout<<_a1<<" "<<_a2<<endl;
 }
private:
  int _a2;
  int _a1;
};
int main() {
  A aa(1);
  aa.Print();
}
A. 输出1  1
B.程序崩溃
C.编译不通过
D.输出1  随机值

因此,_a2先声明,_a2先初始化为随机值。


2.明确禁止隐式转换:explicit关键字

2.1隐式类型转化

#include 
#include 
#include 

using namespace std;

class A
{
public:
	//默认构造函数
	A(int a = 0,int b = 0)
		:_a(a)
		,_b(b)
	{
		//表示默认构造函数被调用过
		cout << "A(int a = 0)" << endl;
	}

	//默认析构函数
	~A()
	{
		_a = 0;

		//表示默认析构函数已被调用
		cout << "~A" << endl;
	}

	//拷贝构造函数
	A(const A& a)
	{
		_a = a._a;

		cout << "A(const A& a)" << endl;
	}

	//赋值重载函数
	A& operator=(const A& a)
	{
		if (this != &a)
		{
			_a = a._a;
		}

		cout << "A& operator=(const A& a)" << endl;
		return *this;
	}

private:
	int _a;
	int _b;
};


int main()
{
	A aa1 = 100;	
	return 0;
}

上述代码中,A aa1 = 100看起来比较奇怪,其实他是合法的。
这其中的过程涉及到了隐式类型转化。过程如下:

  • 将100构造生成同类型临时变量(A),即调用一次构造函数
  • 再调用拷贝构造函数,将临时变量拷贝构造给 aa1

不过上述过程往往会得到编译器的优化(如果编译器比较新的话)
与其先生成临时变量,再拷贝,不如直接对目标进行构造
因此,构造+拷贝构造的过程就被优化为了构造。

此外,对于类中多成员变量的隐式类型转化,c++11也进行了支持

int main()
{
	A aa1 = {1,2};	
	return 0;
}

如果我们不想让隐式类型转换的发生,就可以使用explicit关键字作用于构造函数

class A {
public:
    explicit A(int a) : _a(a) 
    {}
private:
    int _a;
};

2.2 explicit关键字的作用

因此,explicit关键字用于防止构造函数的隐式调用,这意味着必须显式地使用构造函数来创建对象。这样做可以避免因隐式类型转换而引入的错误

2.3 使用场景

  • 提高代码安全性:防止因意外的类型转换而导致的逻辑错误。
  • 增强代码可读性:代码的阅读者可以更清楚地看到类型转换的发生,而不是在不经意间发生。

3.静态成员的独特魅力

3.1 静态成员变量

实现一个类,计算程序中创建出了多少个类对象
考虑下面的代码:

class A
{
public:
	A(int a = 0)
	{
		++count;
	}

	A(const A& aa)
	{
		++count;
	}


	int GetCount()
	{
		return count;
	}

private:
	// 不属于某个对象,所于所有对象,属于整个类
	static int count; // 声明

	int _a = 0;
};

int A::count = 0; // 定义初始化

void func()
{
	A aa1;
	A aa2(aa1);
	A aa3 = 1;

	A aa4[10];
}

int main()
{
	func();//对象的创建
	
	//访问静态成员变量
	//cout << A::count << endl;
	//cout << aa2.count << endl;
	//cout << aa3.count << endl;
	//A* ptr = nullptr;
	//cout << ptr->count << endl;

	
	A aa;
	cout << aa.GetCount()-1 << endl;//显示最终结果

	return 0;
}

其中:

  • 静态成员变量在类的所有对象中是共享的。它们不属于任何一个对象实例,而是属于类本身。静态成员变量在程序开始时被创建,在程序结束时被销毁。

  • 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

int A::count = 0; // 定义初始化
  • 类静态成员可用 类名::静态成员 或者对象.静态成员 来访问
cout << A::count << endl;
cout << aa2.count << endl;

此外,下面代码也没有出错。因为没有发生解引用的操作

A* ptr = nullptr;
cout << ptr->count << endl;
  • 静态成员也是类的成员,受public、protected、private 访问限定符的限制

3.2 静态成员函数

上面调用GetCount需要专门再创建一个对象,非常麻烦。可以使用static来修饰成员函数使其成为静态成员函数

class A
{
public:
	A(int a = 0)
	{
		++count;
	}

	A(const A& aa)
	{
		++count;
	}

	// 静态成员函数 -- 没有this指针
	static int GetCount()
	{
		// _a++; // 不能直接访问非静态成员
		return count;
	}

private:
	// 不属于某个对象,所于所有对象,属于整个类
	static int count; // 声明

	int _a = 0;
};

int A::count = 0; // 定义初始化

void func()
{
	A aa1;
	A aa2(aa1);
	A aa3 = 1;

	A aa4[10];
}

int main()
{
	func();//对象的创建
		
	cout << A::GetCount()<< endl;

	return 0;
}

其中:

  • 静态成员函数可以在没有类的对象实例的情况下被调用
cout << A::GetCount()<< endl;
  • 它们只能访问静态成员变量和其他静态成员函数
    • 因为静态成员函数没有this 指针,不能访问任何 非静态成员
	// 静态成员函数 -- 没有this指针
	static int GetCount()
	{
		// _a++; // 不能直接访问非静态成员
		return count;
	}

4. 匿名对象:一次性的便利

4.1 匿名对象的定义和使用

匿名对象是没有名称的对象实例,通常用于一次性的操作,如函数调用的参数或返回值。它们的生命周期非常短,仅存在于创建它们的表达式中

//假设存在类A
int main()
{
	A();	//此处就是一个匿名对象
	
	return 0;
}

4.2 使用场景

  • 临时操作:适用于需要临时对象完成操作的场景,如函数参数传递。
  • 代码简洁:使用匿名对象可以使代码更加简洁明了。
//1.// 创建一个匿名A对象并立即调用其Print方法
A().Print(); 

//2.
A retA(ret);
return retA;//创建一个对象再返回

return A(ret);//可以直接返回一个匿名对象

5. 友元关系的深入理解

5.1 友元函数

友元函数可以访问类的私有成员,即使它们不是类的成员函数。这对于实现某些操作符重载非常有用。
friend修饰函数时,称为友元函数.

见下面代码:

class A
{
public:
	//声明外部函数 Print 为友元函数
	friend void Print(const A&a);

	A(int val = 100)
		:_val(val)
	{}

private:
	int _val;
};

void Print(const A& a)
{
	cout << a._val << endl;
}

int main()
{
	A aa;
	Print(aa);
}

在类中声明友元函数后,函数即可访问类中的私有成员

注意:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

5.2 友元类

友元类的所有成员函数都可以访问另一个类的私有成员。这在类之间的紧密协作中非常有用。
但是

  • 友元关系是单向的,不具有交换性
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;
};

比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。

Time的声明好像就是在说:Date是我的好朋友,不要阻止他访问我的成员

  • 友元关系不能传递
    如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  • 友元关系不能继承

6. 内部类:隐藏的艺术

6.1 内部类的定义

内部类是定义在另一个类内部的类。这种结构用于隐藏实现细节,使得外部代码不能直接访问内部类的成员。

class A
{
public:
	//B 称作 A 的内部类
	class B
	{
	private:
		int _b;
	}
private:
	int _a;
}

内部类两个重要特性:

  • 内部类 ,跟A是独立,但是受A的类域限制
class A
{
private:
	//B 称作 A 的内部类
	class B
	{
	private:
		int _b;
	}
private:
	int _a;
}
int main()
{
	A::B bb;//无法创建
	return 0;
}

上述代码中,就无法创建内部类对象,因为受A类private的限制。

  • 内部类天生就是外类的友元类

可以在B中访问A的私有成员。

class A
{
private:
	static int k;
	int h = 1;
public:
	// B天生就是A的友元
	class B
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	private:
		int b = 2;
	};
};

int A::k = 1;

int main()
{
	A::B bb;
	return 0;
}

7. 编译器优化:背后的智能

7.1参数优化

见下面代码

void func1(A aa)
{
}

int main()
{
	A aa1 = 1; // 构造+拷贝构造 -》 优化为直接构造
	func1(aa1); // 无优化
	func1(2); // 构造+拷贝构造 -》 优化为直接构造
	func1(A(3)); // 构造+拷贝构造 -》 优化为直接构造
}

编译器都将构造+拷贝构造优化为了直接构造

注意区分引用传参,这是没有进行优化的。

void func2(const A& aa)
{

}

func2(aa1);  // 无优化
func2(2);    // 无优化   调用一次构造
func2(A(3)); // 无优化	调用一次构造

由以上例子,可以总结得出:

  • 尽量使用const &传参

7.2返回值优化

传值返回
例一:

A func3()
{
	A aa;
	return aa; 
}

int main()
{
	func3();//构造+拷贝构造

	A aa1 = func3(); // 拷贝构造+拷贝构造  -- 优化为一个拷贝构造
}

过程见下图:

【c++】类和对象(下)_第1张图片
此外,这种情况请注意区分,下面编译器就不会进行优化。

A func3()
{
	A aa;
	return aa; 
}

int main()
{
	func3();//构造+拷贝构造
	
	A aa1;//构造
	aa1 = func3();//构造+拷贝构造+赋值
}

对比详见下图

【c++】类和对象(下)_第2张图片

例二:

A func4()
{
	return A();
}


int main()
 {

	func4(); // 构造+拷贝构造 -- 优化为构造
	A aa3 = func4(); // 构造+拷贝构造+拷贝构造  -- 优化为构造
 }

由以上例子,可以总结得出:

  • 接收返回值对象,尽量拷贝构造的方式接收,不要赋值接收
  • 函数中返回对象时,尽量返回匿名对象

你可能感兴趣的:(c++,c++)