【C++初阶】类和对象(下)

1024


1. 初始化列表

  1. 在类和对象(上)中,我们实现构造函数时初始化成员变量主要使用函数体内赋值,其实构造函数初始化还有一种方式,就是初始化列表
  2. 初始化列表的使用方式是在函数体之前以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。每个成员变量在初始化列表中只能出现一次,可以认为初始化列表是每个成员变量定义初始化的地方。
    比如上次我们写的Date类的构造函数:
Date()
{
    
    
	cout << "Date()" << endl;
	_year = 2024;
	_month = 10;
	_day = 18;
}

就可以使用初始化列表进行改造

Date()
	: _day(18)
	, _month(10)
	, _year(2024)
{
    
    
	cout << "Date()" << endl;	
}

当然就算不是默认构造函数也可以这样写。

Date(int year, int month, int day)
	: _day(day)
	, _month(month)
	, _year(year)
{
    
    
	cout << "Date(int year, int month, int day)" << endl;
}

同理也可以给缺省

  1. 引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
    前两个变量有一个共同点就是必须在创建时初始化,且初始化之后不能修改(引用不能修改引用的对象),而没有默认构造的类类型对象也是创建时必须传值进行初始化
  2. C++11支持在成员变量声明的位置给缺省值,这个缺省值是给没有显示在初始化列表初始化的成员使用的(就算没有显式写出构造函数,编译器自动给出的)。
#include<iostream>
using namespace std;
class Date
{
    
    
public:
	Date()
		:_day(2)	//对_day进行了初始化
	{
    
    
		cout << "Date() " << _year << " " << _month << " " << _day << " " << endl;
		_month = 2;
	}
	
	void Print()	//使用<<的重载函数是更规范的写法,这里图个方便
	{
    
    
		cout << "Print() " << _year << " " << _month << " " << _day << " " << endl;
	}
private:
	int _year = 1970;
	int _month = 1;
	int _day = 1;
};

	
int main()
{
    
    
	Date d1;
	d1.Print();
	return 0;
}

输出:输出

可以看到,除了给出初始化列表的_day之外,其他都初始化成了缺省值,_month的值也在构造函数内部被改变了。
当构造函数没有在初始化列表中对成员变量进行初始化时,就会使用缺省值进行初始化。

  1. 尽量使用初始化列表初始化,因为不在初始化列表初始化的成员也会走初始化列表(也就是无论如何,变量都会尝试在初始化列表中初始化)。
    如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化(也就是说给缺省值是给初始化列表用的,也是在初始化列表时进行初始化)。
    如果你没有给缺省值,对于没有显示在初始化列表初始化(没有在初始列表初始化说明也没有缺省值)的内置类型成员是否初始化取决于编译器,C++并没有规定;对于没有显式在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
  2. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。

测试:这个代码的结果是什么(假设编译器不会对未显式初始化的内置类型进行初始化)?

#include<iostream>
using namespace std;
class test
{
    
    
public:
	test(int a)
		:_a1(a)
		,_a2(_a1)
	{
    
    }
	
	void Print() {
    
    
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};
int main()
{
    
    
	test aa(1);
	aa.Print();
}

由于初始化列表中对两个内置变量都进行显式初始化,所以缺省值无用了。
接下来的程序乍一看好像是给出a,用a初始化_a1,然后用_a1初始化_a2,但实际上由于在声明变量时(也就是在类的private修饰域中的声明顺序)是先_a2_a1的,所以根据第6点,即使初始化列表中是先初始化的_a1,实际上也是先初始化_a2,而初始化_a2_a1未初始化,为随机值,所以输出应该是1和随机值
运行结果:输出
7. 如果一个指针类型想要给缺省值或者在初始化列表中初始化,可以使用动态内存管理,另外C++提供了更方便好用的动态内存管理关键字 new,这个下篇博客介绍。

#include<iostream>
using namespace std;

class test
{
    
    
public:
	test()
	{
    
    
		cout << _a << endl;
	}

	~test()
	{
    
    
		free(_a);	//不要忘记析构时释放空间,防止内存泄漏
	}
private:
	int* _a = (int*)malloc(sizeof(int) * 5);
};

int main()
{
    
    
	test a;
	return 0;
}

这样可以在创建时就为_a分配好5个 int 的空间。
逻辑

2. 隐式类型转换

  1. C++支持内置类型隐式类型转换为类类型对象,但是需要有相关内置类型为参数的构造函数
  2. 构造函数前面加explicit就可以不支持隐式类型转换。
  3. 类类型的对象之间也可以隐式转换,需要相应的构造函数支持
  4. C++11之后支持了多参数隐式类型转换,除了相应的构造函数支持之外,多参数需要用{}括住,用逗号隔开。
  5. 隐式类型转换进行构造的本质是先使用对应的构造函数构造一个临时对象,再进行拷贝构造给新创建的变量。临时变量具有常性,如果使用隐式类型转换创建的是一个对象引用,就必须使用const修饰这个引用。
#include<iostream>
using namespace std;
class A
{
    
    
public:
	// 构造函数前加explicit就不再支持隐式类型转换
	// explicit A(int a1)
	A(int a1)
		:_a1(a1)
	{
    
    }
	
	//explicit A(int a1, int a2)
	A(int a1, int a2)
		:_a1(a1)
		,_a2(a2)
	{
    
    }
	
	void Print()
	{
    
    
		cout << _a1 << " " << _a2 << endl;
	}
	int Get() const
	{
    
    
		return _a1 + _a2;
	}
private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
    
    
public:
	B(const A& a)	//用于支持使用类A构造类B
		:_b(a.Get())
	{
    
    }
private:
	int _b = 0;
};
int main()
{
    
    
	// 构造一个A的临时对象,再用这个临时对象拷贝构造aa3
	A aa1 = 1;
	aa1.Print();
	
	// 隐式类型转换构建引用,需要用const修饰
	const A& aa2 = 1;
	
	A aa3 = {
    
     2,2 };	//多参数需要用大括号括起来
	
	// aa3隐式类型转换为b对象
	// 原理跟上面类似
	B b = aa3;	
	const B& rb = aa3;
	return 0;
}

补充:在一些比较先进的编译器上,遇到连续构造+拷贝构造时会直接优化为直接构造,也就是说隐式类型转换的两步会直接优化为1步,我们可以通过在构造函数和拷贝构造中加入输出语句来观察。

#include<iostream>
using namespace std;
class A
{
    
    
public:
	A(int a1)
		:_a1(a1)
	{
    
    
		cout << "A(int a1)" << endl;
	}

	A(int a1, int a2)
		:_a1(a1)
		, _a2(a2)
	{
    
    
		cout << "A(int a1, int a2)" << endl;
	}

	A(const A& a)
	{
    
    
		cout << "A(const A& a)" << endl;
		_a1 = a._a1;
		_a2 = a._a2;
	}

	void Print()
	{
    
    
		cout << _a1 << " " << _a2 << endl;
	}
	int Get() const
	{
    
    
		return _a1 + _a2;
	}
private:
	int _a1 = 1;
	int _a2 = 2;
};

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

比如说这个代码,按照我们的预期,应该是先调用一次第一个构造,再调用拷贝构造,然后是第二个构造,再是拷贝构造。但是实际运行起来我们会发现:
VS2022::VS2022
编译器的这个优化对结果自然不会产生影响,但是对于代码效率,特别是对于有存储有大量数据的数组的类来说,能出现显著的提高。
本文最后一章会介绍一些编译器的其他优化。

3. static成员

  1. static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化
  2. 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区,生命周期和类的定义一致。
  3. static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
  4. 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的成员,因为没有this指针,而访问非静态成员都是通过this指针的解引用完成的。
  5. 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
  6. 类域之外也可以访问静态成员,可以通过类名::静态成员 或者 对象,静态成员 来访问静态成员变量和静态成员函数。
    比如要实现一个返回静态成员变量的函数,就可以把这个函数也设置为静态的,这样不需要创建实例化对象就可以访问这个静态成员变量了。
  7. 静态成员也是类的成员,受publicprotectedprivate 访问限定符的限制。
  8. 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,但是静态成员变量不属于某个对象,不会在初始化列表中初始化。

3. 1 静态成员的作用

比如我们要计算一个类被创建了多少个,就可以使用静态成员变量。

#include<iostream>
using namespace std;
class A
{
    
    
public:
	A()
	{
    
    
		++_scount;
	}
	A(const A& t)
	{
    
    
		++_scount;
	}
	~A()
	{
    
    
		--_scount;
	}
	
	// 如果这个函数没有被设置为静态的,除了需要创建对象才能访问到这个函数之外
	// 因为创建的那个用来访问这个函数的对象也会使_scount++,所以会导致得到的结果错误
	static int GetACount()
	{
    
    
		return _scount;
	}
private:
	// 类里面声明
	static int _scount;
};
// 类外面初始化
int A::_scount = 0;

int main()
{
    
    
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);

	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;

	// 编译报错:error C2248: “A::_scount”: 无法访问 private 成员(在“A”类中声明)
	//cout << A::_scount << endl;
	return 0;
}

我们再用这个思路解决一道OJ题
OJ题
这道题的官方解答是使用的位运算,这里不多解释,我们想想怎么使用本篇博客的知识解答这道题。

我们可以创建一个类,这个类中有两个静态变量,分别用于模仿正常情况下使用for循环时的i(循环变量)和sum(最终值),当这个类被创建时,让i++,sum+=i,这样创建n个对象,sum的值就是最终需要的结果,而创建n个对象又可以使用数组,这样这道题就可以解答出来了。

class tmp {
    
    
  public:
    tmp() 
    {
    
    
        ret += n;
        n++;
    }
    static int GetRet() 
    {
    
    
        return ret;
    }
  private:
    static int n;	//相当于循环中的 i
    static int ret;	//相当于sum

};
int tmp::n = 1;
int tmp::ret = 0;

class Solution {
    
    
  public:
    int Sum_Solution(int n) {
    
    
        tmp a[n];	//实例化n个tmp类的对象,相当于进行了for循环
        return tmp::GetRet();
    }
};

4. 友元

  1. 友元提供了一种突破类访问限定符封装的方式,友元分为友元函数友元类,需要把友元声明放到这个类的里面,友元声明就是在函数声明或者类声明的前面加friend
  2. 外部友元函数可访问类的私有和保护成员(公有当然也可以),友元函数仅仅是一种声明,并不是类的成员函数
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数。
  5. 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
  6. 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
  7. 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
  8. 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
    过多的友元会使封装遭到严重破坏,如果需要的友元太多,不妨考虑是否可以用get函数代替。
#include<iostream>
using namespace std;
// 前置声明,否则A的在声明友元函数时编译器不认识B
class B;
class A
{
    
    
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
    
    
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _b1 = 3;
	int _b2 = 4;
};

void func(const A& aa, const B& bb)
{
    
    
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}
int main()
{
    
    
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}

func()函数同时被两个类声明为友元函数,所以可以同时访问两个类的私有成员。

5. 内部类

  1. 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
  2. 内部类是一个独立的类,跟定义在全局相比,它只是受外部类类域限制和访问限定符限制,所以外部类实例化的对象中不包含内部类
  3. 内部类默认是外部类的友元类。
  4. 内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
#include<iostream>
using namespace std;
class A
{
    
    
public:
	//B是A的内部类
	class B		// B默认就是A的友元
	{
    
    
	public:
		void test(const A& a)
		{
    
    
			cout << _k << endl; 
			cout << a._h << endl;
		}
	};

private:
	static int _k;
	int _h = 1;
};
int A::_k = 1;

int main()
{
    
    
	cout << sizeof(A) << endl;
	A::B b;	//使用::访问类B,如果类B在A中被private或protected修饰,就不能这样访问
	A aa;	//aa中是没有B的实例化对象的
	b.test(aa);
	return 0;
}

6. 匿名对象

类型(实参) 定义出来的对象叫做匿名对象,而之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象。
匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。

#include<iostream>
using namespace std;

class A
{
    
    
public:
	A(int a = 0)
		:_a(a)
	{
    
    
		cout << "A(int a)" << endl;
	}
	~A()
	{
    
    
		cout << "~A()" << endl;
	}
private:
	int _a;
};

class Solution {
    
    
public:
	int Sum_Solution(int n) {
    
    
		//...
		return n;
	}
};

int main()
{
    
    
	A aa1;

	// 不能像下面这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
	//A aa1();
		
	// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
	// 但是它的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
	A();
	A(1);
	A aa2(2);
	// 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
	Solution().Sum_Solution(10);	//这样不需要创建一个有名对象就可以
	return 0;
}

运行结果为:
运行结果

7. 对象拷贝时的编译器优化

现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。

#include<iostream>
using namespace std;

class A
{
    
    
public:
	A(int a = 0)
		:_a1(a)
	{
    
    
		cout << "A(int a)" << endl;
	}

	A(const A& aa)
		:_a1(aa._a1)
	{
    
    
		cout << "A(const A& aa)" << endl;
	}

	A& operator=(const A& aa)
	{
    
    
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
    
    
			_a1 = aa._a1;
		}
		return *this;
	}

	~A()
	{
    
    
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};

void f1(A aa)
{
    
    }

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

int main()
{
    
    
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;

	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);

	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));	//原本是构造一个匿名对象在传参(拷贝)
	cout << endl;
	cout << "***********************************************" << endl;

	// 传值返回
	//返回时一个表达式中,连续拷贝构造 + 拷贝构造->优化一个拷贝构造(vs2019 debug)
	// 一些编译器会优化得更厉害,进行跨行合并优化,直接变为构造。(vs2022 debug)
	f2();	//原本是在函数中构造一个对象,再传值返回(拷贝)
	cout << endl;

	A aa2 = f2();
	cout << endl;

	//一个表达式中,连续拷贝构造 + 赋值重载->无法优化
	aa1 = f2();	//构造匿名对象,再赋值重载
	cout << endl;
	return 0;
}

VS2022的运行结果:结果
编译器优化一般不会影响代码的结果(除了像上面的代码这样刻意展现),所以了解就好。

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会持续更新更多优质文章!

猜你喜欢

转载自blog.csdn.net/fhvyxyci/article/details/143205820