前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++打怪升级(六)- 类和对象入门3

C++打怪升级(六)- 类和对象入门3

作者头像
怠惰的未禾
发布2023-04-27 21:48:58
6150
发布2023-04-27 21:48:58
举报
文章被收录于专栏:Linux之越战越勇

前言

本节继续进行类和对象的介绍,再坚持坚持,前方就是休息室了!


对构造函数的补充

我们知道,类对象定义时会自动调用类的构造函数完成对类对象成员变量的初始化。 前文我们并没有对构造函数进行进一步的探讨,即类对象创建时类成员变量具体是在构造函数哪里初始化的? 其实,构造函数内对成员变量赋值的操作并不能称之为对成员变量的初始化,而是只能称之为对成员变量赋初值。

代码语言:javascript
复制
class Date {
public:
	//默认 构造
	Date(int year = 1, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

那么成员变量的定义发生在构造函数的哪里呢? 这需要引入构造函数的初始化列表的概念了:

初始化列表

初始化列表格式:

以一个冒号:开始,接着是一个以逗号,分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式; : _member1(expression1), _member(expression2), ...


初始化列表特性

每个构造函数都有一个初始化列表用于对每个成员变量进行初始化; 初始化列表默认是由编译器隐式生成的,如果一个成员变量被我们显式的在初始化列表中写了,那么编译器就不在初始化列表中生成该成员变量的默认初始化了; 每一个类对象的成员变量的定义都发生在构造函数的初始化列表中; 在调用构造函数时,先进行初始化列表中的操作,在进行构造函数体内的操作;

代码语言:javascript
复制
class Date {
public:
	//默认 构造
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day){
	}
private:
	int _year;
	int _month;
	int _day;
};

对于隐式的(默认的)初始化列表: 对于内置类型,隐式初始化列表会把内置类型初始化为随机值或0值,具体是哪一种与具体的编译器有关,C++标准也没有对此进行规定; 对于自定义类型,隐式初始化列表会直接调用自定义类型变量的自己的默认构造函数进行初始化;

代码语言:javascript
复制
class A {
public:
	A(int a = 0) {
		cout << "构造函数: A(int a = 0)" << endl;
		_a = a;
	}
private:
	int _a;
};

class B {
public:
	B(int b = 1) :_b(b){
		cout << "构造函数: B(int b = 1)" << endl;
	}
private:
	int _b;
	A _aa;
};

int main() {

	B b1(10);
	return 0;
}

每个成员变量在初始化列表中只能出现一次 即每个成员变量初始化只能初始化一次

代码语言:javascript
复制
class A {
public:
	A(int a = 0) 
		:_a(a)
		,_a(a){//error
	}
private:
	int _a;
};

int main() {

	A a1;
	return 0;
}

必须放在初始化列表位置进行初始化的情况

类中有引用成员变量、const成员变量

引用成员变量必须在定义时初始化,const修饰的成员变量也必须在定义时初始化,二者都只能初始化一次; 类成员变量又是在构造函数初始化列表进行初始化的,故引用成员变量和const修饰成员变量都必须在初始化列表进行显式初始化; const成员变量错误举例:

const成员变量正确举例:

引用成员变量错误举例:

引用成员变量正确举例:

类中有自定义类型成员,且该类没有默认构造函数 因为在创建类对象时先调用构造函数,在构造函数的初始化列表会自动调用自定义类行变量的默认构造函数,如果该自定义类型没有默认构造函数程序就会报错;

代码语言:javascript
复制
class A {
public:
	A(int a) 
		:_a(a){
	}
private:
	int _a;
};

class B {
public:
	B(int b = 1) :_b(b) {
		;
	}
private:
	int _b;
	A _aa;
};

int main() {

	B b1;
	return 0;
}
代码语言:javascript
复制
class A {
public:
	A(int a) 
		:_a(a){
	}
private:
	int _a;
};

class B {
public:
	/*B(int b = 1) :_b(b) {
		;
	}*/
private:
	int _b;
	A _aa;
};

int main() {

	B b1;
	return 0;
}

定义B类的对象时会调用B的默认构造,在B的默认构造初始化列表会调用A的默认构造, 但A没有默认构造,所以报错,但报的是没有B的默认构造函数; 不定义B的对象并不会报错,因为没有调用B的默认构造;


我们需要对没有默认构造函数的自定义类型成员变量显式的在构造函数的初始化列表中进行初始化

代码语言:javascript
复制
class A {
public:
	A(int a) 
		:_a(a){
	}
private:
	int _a;
};

class B {
public:
	B(int b = 1, int a = 10) :_b(b) ,_aa(a){
		;
	}
private:
	int _b;
	A _aa;
};

int main() {

	B b1;
	return 0;
}

尽量使用初始化列表初始化

因为不管是否使用初始化列表,成员变量都会先通过初始化列表进行初始化; 初始化列表并不能解决所有问题,初始化列表与构造函数内赋初值常常会混合使用; 比如初始时动态申请空间时:

代码语言:javascript
复制
class Stack {
public:
	//普通构造
	Stack(int capacity = 4) 
		:_top(0)
		,_capacity(capacity){

		_array = (int*)malloc(sizeof(int) * capacity);
		if (_array == nullptr) {
			perror("Stack(int capacity = 4) malloc file");
			exit(-1);
		}
	}
	~Stack() {
		free(_array);
		_array = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _array;
	size_t _top;
	size_t _capacity;
};

int main() {
	Stack st;
	return 0;
}

成员变量的声明顺序与初始化顺序的关系

初始化列表中 变量实际定义初始化 的顺序与变量的声明顺序相同,也就是说变量声明的顺序决定了变量初始化列表初始化的顺序,而与初始化列表中变量书写的顺序无关 这可能与编译器的底层实现有关

代码语言:javascript
复制
class A {
public:
	A(int a = 1)
		:_b(a)
		,_a(_b) {
	}
private:
	int _a;
	int _b;
};


int main() {

	A a1;
	return 0;
}

类的默认构造函数尽量是由我们显式提供的全缺省的默认构造函数

因为全缺省的默认构造函数功能十分强大,十分好用; 我们在创建类对象时既可以不传参数,完全使用缺省参数,也可以传一部分参数,使用部分缺省参数,也可以传全部参数,不使用缺省参数;

代码语言:javascript
复制
class A {
public:
	A(int a = 1, int b = 2, int c = 3)
		:_a(a)
		,_b(b)
		,_c(c){
	}
private:
	int _a;
	int _b;
	int _c;
};
int main() {

	A a1;
	A a2(10);
	A a3(10, 20);
	A a4(10, 20, 30);
	return 0;
}


初始化列表总结

构造函数核心–初始化列表 构造函数完成初始化的功能可以分为两部分:

1.初始化列表进行的 定义初始化 2.构造函数体内的对初始化列表已定义变量的初始化,或者说赋值更好

每个类成员变量默认(隐式的)都会经过初始化列表, 我们也可以显式的在初始化列表中写, 也就是说,不管我们在初始化列表中是否显式对类成员变量进行定义初始化,成员变量都会在初始化列表被定义初始化

类对象定义时,类成员变量整体 定义初始化 细化类成员变量的 定义初始化: 类成员变量的 定义初始化 发生在初始化列表阶段 即类成员变量在初始化列表进行 定义初始化 而在构造函数内进行的是类成员变量的赋值和其他必要的操作,故在构造函数内进行的不能称之为 定义初始化 ,只能叫做赋初值 毕竟一个类成员变量在生命周期内只能定义一次

对于普通变量来说, 在不在初始化列表显式定义初始化 都可以,因为普通变量 定义初始化 之后还可以在构造函数内修改

对于定义之后不可修改的变量(const修饰的、引用的): 我们必须显式的在初始化列表进行定义初始化,而不能在构造函数体内进行初始化,在函数体内称为赋值也许更好理解 因为所有变量都已经在初始化列表定义初始化过了

对于自定义类型,初始化列表时会调用该类型的默认构造函数, 如果该类没有默认构造函数我们就需要在初始化列表手动对该自定义类型进行定义初始化,不然就会报错(错误是:找不到该自定义类型的默认构造函数)


explicit关键字

explicit关键字修饰的构造函数不能发生隐式类型转换

隐式类型转换

我们知道在不同类型变量进行赋值操作时会发生隐式类型转换,对于类类型也是如此;

代码语言:javascript
复制
double d = 3.14;
int a = d;

浮点型变量d赋值给整型变量a发生隐式类型转换: 首先生成一个匿名整型临时变量假设叫tmp,这个临时变量的值是浮点型变量d的整数部分即3,临时变量再赋值给整型变量a

代码语言:javascript
复制
int a = 3.14;

浮点型字面值3.14赋值给整型变量a发生隐式类型转换: 首先生成一个匿名临时变量假设叫tmp,这个临时变量的是浮点型字面值的整数部分即3,临时变量再赋值给整型变量a

代码语言:javascript
复制
double d = 3.14;
const int& rd = d;

整型引用变量rd引用浮点型变量d发生隐式类型转换: 首先生成一个匿名临时整型变量,这个匿名临时变量的值是浮点型变量d的整数部分,整型引用变量引用的是这个匿名临时引用变量;同时匿名临时变量具有常性,所以整型引用变量rd需要const修饰;

单参数构造函数的隐式类型转换

在C++98中,支持单参数构造函数的隐式类型转换
代码语言:javascript
复制
class A {
public:
	A(int a)
		:_a(a) {
		cout << "构造: A(int a)" << endl;
	}
	A(A& a) {
		cout << "拷贝构造: A(A& a)" << endl;
		_a = a._a;
	}
private:
	int _a;
};
代码语言:javascript
复制
int main() {
	A a1(10);//构造
	A a4 = a1;//拷贝构造
	return 0;
}

代码语言:javascript
复制
int main() {
	A a2 = 10;//隐式类型转换 10 --> 匿名临时A类对象 --> a2;构造+拷贝构造
	//等价于	const A tmp(10); A a2(tmp);
	return 0;
}

这里是整型字面值赋值给类对象a2,发生隐式类型转换: 先生成一个匿名临时A类对象调用构造函数,在用这个临时变量初始化类对象调用拷贝构造


代码语言:javascript
复制
int main() {
	const A& a3 = 100;//隐式类型转换 ;引用的不是整型100,引用的是临时匿名A类对象
	//等价于	const A tmp(100); const A a2(tmp);
	return 0;
}

整型字面值100被类对象a3引用,发生隐式类型转换: 首先创建一个匿名临时类对象调用构造函数类类型引用变量a3引用这个匿名临时类对象


编译器对创建类对象时隐式类型转换的可能的优化

刚才我们分析了创建类对象或类对象参与的赋值操作时隐式类型转换是如何发生的; 我么可以看到隐式类型转换的发生会创建临时变量,这其实是额外的开销,如今的编译器大多对其进行了优化;

代码语言:javascript
复制
int main() {
	A a2 = 10;//隐式类型转换 10 --> 匿名临时A类对象 --> a2;构造+拷贝构造
	//等价于	const A tmp(10); A a2(tmp);
	return 0;
}

本来隐式类型转换产生了临时类对象,然后临时类对象在拷贝初始化a2; 这里涉及到产生临时类对象的拷贝构造,拷贝初始化时的拷贝构造 编译器呢如今可以直接将这两个步骤优化为一个步骤:直接构造类对象a2; 相当于A a2 = 10; ----> A a2(10);只调用一次拷贝构造即可完成类对象a2的初始化;


编译器是对连续步骤中的优化,对于分开的步骤,考虑到创建的类对象可能还有其他用途,编译器并不会对分开的步骤进行优化;

代码语言:javascript
复制
int main() {

	A tmp(100);//构造函数
	A a2(tmp);//拷贝构造
	return 0;
}

这里是显式分开的步骤创建类对象a2: 先创建类对象tmp,调用构造函数; 再创建类对象a2,调用拷贝构造 编译器无法把拷贝构造这一步优化掉,因为类对象tmp可能会在程序后面使用;


explicit关键字对隐式类型转换的限制

使用explicit修饰的单参数构造函数不能发生隐式类型转换了; 于是一些操作就被禁止了;

代码语言:javascript
复制
class A {
public:
	explicit A(int a)
		:_a(a) {
		cout << "构造: A(int a)" << endl;
	}
	A(A& a) {
		cout << "拷贝构造: A(A& a)" << endl;
		_a = a._a;
	}
private:
	int _a;
};
代码语言:javascript
复制
int main() {
	A a2 = 10;//隐式类型转换 10 --> 匿名临时A类对象 --> a2;构造+拷贝构造
	//等价于	const A tmp(10); A a2(tmp);
	return 0;
}
代码语言:javascript
复制
const A& a3 = 100;//隐式类型转换 ;引用的不是整型100,引用的是临时匿名A类对象
	//等价于	const A tmp(100); const A& a2 = tmp;

多参数构造函数的隐式类型转换

在C++11中,支持多参数构造函数的隐式类型转换

C++11中支持了多参构造函数的隐式类型转换

代码语言:javascript
复制
class A {
public:
	A(int a = 1,int b = 1, int c = 1)
		:_a(a)
		,_b(b)
		,_c(c){
		cout << "构造: A(int a = 1,int b = 1, int c = 1)" << endl;
	}
	A(A& a) {
		cout << "拷贝构造: A(A& a)" << endl;
		_a = a._a;
		_b = a._b;
		_c = a._c;
	}
private:
	int _a;
	int _b;
	int _c;
};
代码语言:javascript
复制
int main() {
	A a1(10, 20, 30);//构造
	return 0;
}

类对象创建时调用的普通构造函数;

代码语言:javascript
复制
int main() {

	A a2 = { 1,2,3 };//构造+拷贝构造 --> 优化为 构造
	return 0;
}

这里花括号内的三个整型字面值为类对象a2赋值,发生隐式类型转换: 首先花括号内三个整形字面值作为参数创建匿名临时类对象并调用构造函数,然后这个临时类对象再拷贝赋值给类对象a2,并调用拷贝构造函数


代码语言:javascript
复制
int main() {
    
    const A& a3 = { 1,2,3 };//构造
	return 0;
}

这里花括号里三个整形字面值被类类型引用变量a3引用时,发生隐式类型转换: 首先花括号内三个整形字面值作为参数创建匿名临时类对象并调用构造函数,然后类类型引用变量a3再引用这个临时变量; 并且由于临时对象具有常性const,多以类类型引用变量需要用const修饰;


编译器对创建类对象时隐式类型转换的可能的优化

编译器是对连续步骤中的优化,对于分开的步骤,考虑到创建的类对象可能还有其他用途,编译器并不会对分开的步骤进行优化;

代码语言:javascript
复制
int main() {

	A a2 = { 1,2,3 };//构造+拷贝构造 --> 优化为 构造
	return 0;
}

explicit关键字对隐式类型转换的限制

使用explicit修饰的单参数构造函数不能发生隐式类型转换了; 于是一些操作就被禁止了,这对于多参数的构造函数来说同样适用;

代码语言:javascript
复制
class A {
public:
	explicit A(int a = 1, int b = 1, int c = 1)
		:_a(a)
		, _b(b)
		, _c(c) {
		cout << "构造: A(int a = 1,int b = 1, int c = 1)" << endl;
	}
	A(A& a) {
		cout << "拷贝构造: A(A& a)" << endl;
		_a = a._a;
		_b = a._b;
		_c = a._c;
	}
private:
	int _a;
	int _b;
	int _c;
};
代码语言:javascript
复制
int main() {

	A a2 = { 1,2,3 };//构造+拷贝构造 --> 优化为 构造
	return 0;
}

代码语言:javascript
复制
int main() {
    
    const A& a3 = { 1,2,3 };//构造
	return 0;
}

static修饰类成员

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

静态成员变量一定要在类外进行定义初始化 ; 静态成员变量的定义初始化时需要指定作用域为类的作用域,以此来区分全局静态变量; 静态成员变量并不在类中,即不是每个类对象中都有一份静态成员变量,而是在程序运行时就在静态区创建,整个程序运行期间只有一份; 静态成员函数不在类中,也不在公共代码段,而是和静态成员变量一样也在静态区;

代码语言:javascript
复制
class A {
public:
	A(int a = 1):_a(a){}
private:
	int _a;
	static int _b;
};

int A::_b = 0;

int main() {
	A aa;
	int size1 = sizeof(A);
	int size2 = sizeof(aa);
	return 0;
}

静态的成员变量 不属于任何一个对象,在静态区只有一份,任何一个对象都能访问到; 下面的只是说明静态成员变量_b是在类A域中的_b起作用的是类型,而不是对象本身,这与成员函数非常相似;

代码语言:javascript
复制
class A {
public:
	A(int a)
		:_a(a) {
	}
	static int _b;//声明
private:
	int _a;
};
int A::_b = 0;//定义初始化

int main() {
	A a1(1);//构造
	cout << A::_b << endl;
	cout << a1._b << endl;
	A* ptr = &a1;
	cout << ptr->_b << endl;
	ptr = nullptr;
	cout << ptr->_b << endl;
	return 0;
}

静态成员特性

静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区 静态成员变量在类中用static修饰进行声明,在类外进行定义初始化,并且在类外不加static修饰,需要指定类域;

代码语言:javascript
复制
class A {
public:
	A(int a = 1):_a(a){}
private:
	int _a;
	static int _b;
};

int A::_b = 0;

类静态成员变量的访问方式

类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问

代码语言:javascript
复制
class A {
public:
	A(int a = 1):_a(a){}
//private:
	int _a;
	static int _b;
};

int A::_b = 0;

int main() {
	A aa;
	cout << A::_b << endl;
	cout << aa._b << endl;
	return 0;
}

对静态成员函数的调用

静态成员也是类的成员,受publicprotectedprivate 访问限定符的限制

静态成员函数没有隐藏的this指针,不能访问任何非静态成员 我们知道,普通的类成员函数第一个形参都是隐式的this指针,而静态成员函数形参没有this指针; 而this指针指向的是类对象,故静态成员函数无法访问类对象的普通成员变量,无法调用普通成员函数; 只能访问静态成员变量和调用静态成员变量;

代码语言:javascript
复制
class A {
public:
	A(int a = 1) :_a(a) {}
	//静态成员函数
	static void Function1() {
		cout << _b << endl;
	}
	//非静态成员函数
	void Function2() {
		cout << _a << " " << _b << endl;
	}
private:
	int _a;
	static int _b;
};
int A::_b = 0;
int main() {

	A aa;
	aa.Function1();
	A::Function1();

	aa.Function2();
	//A::Function2();//error
	return 0;
}

统计程序中创建的类对象个数

类对象的创建会调用构造函数,要么是直接构造,要么是拷贝构造; 我们显式的写出构造函数和拷贝构造函数

借助全局变量在显式的构造函数和拷贝构造函数内计数;

代码语言:javascript
复制
int Count = 0;
class A {
public:
	A(int a)
		:_a(a) {
		Count++;
	}
	A(A& a) {
		Count++;
		_a = a._a;
	}
private:
	int _a;
};

int main() {
	A a1(1);//构造
	cout << Count << endl;//Count == 1
	A a2 = 1;//构造+拷贝构造 --> 直接构造
	cout << Count << endl;//Count == 2

	const A& a3 = 1;//构造
	cout << Count << endl;//Count == 3
	return 0;
}

使用static成员变量在显式的构造函数和拷贝构造函数内计数:

代码语言:javascript
复制
class A {
public:
	A(int a)
		:_a(a) {
		Count++;
	}
	A(A& a) {
		_a = a._a;
		Count++;
	}
	static int Count;//声明
private:
	int _a;
};

int A::Count = 0;//定义初始化
代码语言:javascript
复制
int main() {
	A a1(1);//构造
	cout << A::Count << endl;//Count == 1
	A a2 = 1;//构造+拷贝构造 --> 直接构造
	cout << A::Count << endl;//Count == 2
	const A& a3 = 1;//构造
	cout << A::Count << endl;//Count == 3
	return 0;
}

传值传参
代码语言:javascript
复制
//传值传参
void Func1(A a) {
	;
}
int main() {
	A a1(1);//构造
	cout << A::Count << endl;//Count == 1
	Func1(a1);
	cout << A::Count << endl;//Count == 2
	return 0;
}

传值返回
代码语言:javascript
复制
//传值返回
int main() {
	A a1(1);//构造
	cout << A::Count << endl;//Count == 1
	Func2();
	cout << A::Count << endl;//Count == 3
	return 0;
}

传引用传参
代码语言:javascript
复制
//传引用传参
class A {
public:
	A(int a) :_a(a) {
		Count++;
	}
	A(A& a) {
		_a = a._a;
		Count++;
	}
	static int Count;//声明
private:
	int _a;
};
int A::Count = 0;//定义初始化
//传引用传参
void Func3(A& a) {
	;
}
int main() {
	A a1(1);//构造
	cout << A::Count << endl;//Count == 1
	Func3(a1);
	cout << A::Count << endl;//Count == 1
	return 0;
}

传引用返回 - 静态局部变量
代码语言:javascript
复制
//传引用返回
A& Func5() {
	static A a(1);
	return a;
}
int main() {
	Func5();
	cout << A::Count << endl;//Count == 1
	return 0;
}

传引用返回 - 静态全局变量
代码语言:javascript
复制
static A a(1);
//传引用返回
A& Func5() {
	return a;
}
int main() {
	cout << A::Count << endl;//Count == 1
	Func5();
	cout << A::Count << endl;//Count == 1
	return 0;
}

类封装特点的体现

一般类中的成员变量我们在类外是不能直接访问的,静态成员变量也是如此; 所以我们通常会将成员变量声明为私有的,我们在通过成员函数间接得到需要的成员变量的值;

代码语言:javascript
复制
class A {
public:
	A(int a)
		:_a(a) {
		Count++;
	}
	A(A& a) {
		Count++;
		_a = a._a;
	}
	static int GetCount() {
		return Count;
	}
private:
	int _a;
	static int Count;//声明
};

int A::Count = 0;//定义初始化
int main() {
	A a(10);
	cout << A::GetCount() << endl;
	return 0;
}

要求对象只能在栈上创建

类对象创建时都要调用构造函数;

代码语言:javascript
复制
class A {
public:
	A(int a)
		:_a(a) {
	}
private:
	int _a;
};

类对象可以创建在栈上:A a1(10); 创建在堆区:A* a2 = new A; 创建在静态区:static A a3(10); 这三种创建方式都要调用构造函数,如果限制只能在栈上创建对象,我们可以把构造函数用private修饰,使得外部不能够调用构造函数,然后再写一个类对象在栈上创建的函数,并返回这个类对象的拷贝即可注意拷贝构造函数并不是私有的,即拷贝构造在类外可以调用,用于类对象返回时对临时对象进行拷贝构造; 如果拷贝构造也设置成私有的,那么在类外无法调用拷贝构造,也就无法在类外进行拷贝构造,导致类对象返回无法以拷贝构造的方式创建临时类对象,也就是直接无法在类外创建对象了,这样路就全给堵死了

代码语言:javascript
复制
//要求对象只能在栈上创建
class A {
public:
	static A GetObj(int a) {
		A aa1(a);
		return aa1;
	}
private:
	A(int a)
		:_a(a) {
	}
private:
	int _a;
};

int main() {
	/*static A a1(1);//在静态区创建类对象
	A a2(1);//在栈上创建类对象
	A* a3 = new A;*///在堆区创建类对象

	A a4 = A::GetObj(1);
	return 0;
}

有关变量生命周期的分析

static修饰:储存在静态区

局部静态变量生命周期变为程序运行期间; 全局静态变量生命周期为程序运行期间,且影响全局变量的链接属性,使得全局变量只能在本文件中被找到,独属于本文件;

全局域:储存在静态区

全局变量声明周期程序运行期间;

局部域:储存在栈区

局部变量生命周期从进入局部域开始,到出局部域为止;

动态申请的空间:储存在堆区

生命周期从申请开始,知道申请者手动释放;

常量:储存在常量区

字面值常量,生命周期为程序运行期间;


友元

C++引入了类的概念,类体现了C++的封装的特点,封装就是类内对类外的部分隐藏,类外无法自由自在的对类内成员进行访问和修改; 这总体来说是好的,类的隐蔽特点避免了很多不安全的隐患; 但是某些时候也确实对类成员的访问形成了限制导致很不方便,为了应对必要的类外普通函数对类内成员的访问同时尽量不破坏类的封装特点,便引入了友元的概念;

友元是针对类外的函数或另一个类来说的,分为友元函数和友元类

友元说到底还是破坏了类的封装,一般对于友元函数和友元类使用不多;

友元函数

友元函数概念

类外的普通函数,在类内任意位置写上该普通函数的声明,再声明前加上friend关键字修饰; 例如:

代码语言:javascript
复制
class A {
public:
    friend void Function(const A& a);
private:
    int _a = 1;
};
void Function(const A& a) {
    cout << a._a << endl;
}

int main() {
    A a;
    Function(a);
    return 0;
}

友元函数我们在运算符重载那里已经提前提到了,对于流插入运算符<<和流提取运算符>>的重载必须要使用友元函数来完成;

代码语言:javascript
复制
class Date {
public:
	friend ostream& operator<<(ostream& output, const Date& d);
	friend istream& operator>>(istream& input, Date& d);
private:
	int _year;
	int _month;
	int _day;
};

ostream & operator<<(ostream & output, const Date & d) {
	output << d._year << "/" << d._month << "/" << d._day << endl;
	return output;
}
istream& operator>>(istream& input, Date& d) {
	input >> d._year >> d._month >> d._day;
	return input;
}

对友元函数的说明

友元函数可访问类的私有和保护成员,但不是类的成员函数; 友元函数不能只用const修饰,类内的声明要与类外的定义匹配,即要加const,就都加上const

代码语言:javascript
复制
class A {
public:
    friend const void Function(const A& a);
private:
    int _a = 1;
};
const void Function(const A& a) {
    cout << a._a << endl;
}

int main() {
    A a;
    Function(a);
    return 0;
}

友元函数可以在类定义的任何地方声明,不受类访问限定符限制;

一个函数可以是多个类的友元函数;

代码语言:javascript
复制
class A {
public:
    friend class B;
    friend void Function(const A& a, const B& b);
private:
    int _a = 1;
};

class B {
public:
    friend void Function(const A& a, const B& b);
private:
    int _b = 1;
};

void Function(const A& a, const B& b) {
    cout << a._a << endl;
    cout << b._b << endl;
}

int main() {
    A a;
    B b;
    Function(a, b);
    return 0;
}

友元函数的调用与普通函数的调用原理相同 ;


友元类

友元类,即一个类可以访问另一个类的成员;

代码语言:javascript
复制
class A {
public:
    friend class B;
private:
    int _a = 1;
};

class B {
public:
    void Function(const A& a) {
        cout << a._a << endl;
        cout << _b << endl;
    }
private:
    int _b = 2;
};

int main() {
    A a;
    B b;
    b.Function(a);
    return 0;
}

友元类特性

友元关系是单向的,不具有交换性

即A是B的友元,而B并不是A的友元

友元关系不具有传递性

即A是B的友元,B是C的友元,而A并不是C的友元

友元关系不能继承


内部类

内部类概念

定义在一个类内部的类称之为内部类,从形式上看是一个类包含者另一个类;

代码语言:javascript
复制
class A {
public:

    class B {
    public:
        void Function(A& aa) {
            cout << aa._a << endl;
            cout << _b << endl;
        }
    private:
        int _b = 2;
    };

private:
    int _a = 1;
};

int main() {
    A a;
    A::B b;
    b.Function(a);
    return 0;
}

内部类特性

先来看看外部类的大小与内部类的大小之间有没有什么关系:

sizeof(外部类)计算的是外部类的大小,和内部类没有任何关系;

代码语言:javascript
复制
class A {
public:
    class B {
    public:
        void Function(const A& aa) {
            cout << aa._a << endl;
            cout << _b << endl;
        }
    private:
        int _b = 2;
    };

private:
    int _a = 1;
};

int main() {

    A::B b;
    cout << sizeof(A) << endl;
    cout << sizeof(A::B) << endl;
    return 0;
}

内部类是一个独立的类,它不属于外部类,不能通过外部类的对象去访问内部类的成员


内部类天生是外部类的友元类,内部类可以通过外部类的对象参数来访问外部类中的所有成员,反之则不成立; 内部类定义在外部类的public、protected、private时,对内部类的使用会受到访问限定符的影响,这一点与内部类外部类的其他成员相同;

public修饰时在类外指定外部类类域即可使用内部类成员;

private修饰时则在类外不能使用内部类任何成员;


内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名;

代码语言:javascript
复制
class A {
public:
    static int count;
    class B {
    public:
        void Function() {
            cout << count << endl;
            cout << _b << endl;
        }
    private:
        int _b = 2;
    };

private:
    int _a = 1;
};
int A::count = 0;
int main() {

    A::B b;
    b.Function();
    return 0;
}

匿名对象

匿名对象,也是对象,只是没有名字,常常用于简化步骤; 前面我们基本都在接触有名字的对象,接下来瞅瞅匿名对象的使用,认识一下;

代码语言:javascript
复制
class A {
public:
	A(int a = 1)
		:_a(a) {}
	void Print() {
		cout << _a << endl;
	}
private:
	int _a;
};

先给出有名对象

代码语言:javascript
复制
int main() {
	A a1;
	A a2(10);
	A a3 = 10;
	//A a4();//error
	return 0;
}

匿名对象概念

生命周期只有定义的那一行,这一行结束生命周期也就结束了,然后就被销毁;

类型紧接着一个括号,括号里可以有参数,也可以无参数;

代码语言:javascript
复制
int main() {
	//匿名对象
	//生命周期只有一行,一般作用不大,但有时比较有用:
    int();//这个匿名整型变量是0
    int(10);
	A();
	A(10);
    cout << int() << endl;
	cout << int(10) << endl;
	A().Print();
	A(10).Print();
    return 0;
}

匿名对象可能的用途

优化减少类成员函数调用步骤

代码语言:javascript
复制
int main(){
	//原本:只为了调用类内函数而创建类A对象a
	A a(20);
	a.Print();
	//优化
	A().Print();
	A(10).Print();
    return 0;
}

编译器对拷贝对象的优化

编译器的优化 – 需要记住

编译器对涉及类的构造和拷贝构造连续的步骤可能会进行优化,省去中间某个步骤,提高效率; 这就要求我们在与类对象创建时尽可能以少量步骤来完成同样的功能 这些优化当然是在不影响正确性的前提 下进行的

代码语言:javascript
复制
class A {
public:
	A(int a = 1)
		:_a(a){
		cout << "构造函数: A(int a)" << endl;
	}
	A(const A& a) {
		cout << "拷贝构造: A(const A& a)" << endl;
		_a = a._a;
	}
private:
	int _a;
};

对 单参数或多参数构造函数的隐式类型转换 可能的优化

代码语言:javascript
复制
int main() {
	//对 单参数或多参数构造函数的隐式类型转换 可能的优化
	A a1 = 10;//构造+拷贝构造 --> 构造
}

对 传参时 可能的优化

代码语言:javascript
复制
void Func1(A a) {
	;
}
int main() {
	//对 传参时 可能的优化
	A a2(10);
	Func1(a2);//先构造在拷贝构造

	Func1(A(10));//先构造再拷贝构造 --> 直接构造
}

对 函数返回时 可能的优化

普通优化

代码语言:javascript
复制
A Func2() {
	A a(10);
	return a;
}
int main() {
	//对 函数返回时 可能的优化
	Func2();//构造+拷贝构造
	A ret = Func2();//构造+拷贝构造+拷贝构造 --> 构造+拷贝构造
}

极致优化

代码语言:javascript
复制
//传值的极致优化
A Func3() {
	return A(10);//构造+拷贝构造
}
int main() {
	//传值返回极致优化
	A ret = Func3();//构造+拷贝构造+拷贝构造 --> 构造
}

编译器不进行优化的情况

编译器优化也不是无脑优化的,编译器的优化是以正确性为基底的,也就是说编译器不做可能会导致之后程序错误的优化;

代码语言:javascript
复制
A Func2() {
	A a(10);
	return a;
}
int main() {
	//下方编译器无法优化
	A _ret;
	_ret = Func2();//构造+构造+拷贝构造
}

结语

本节主要介绍了类和对象相关概念进行的补充,比如初始化列表,友元,内部类,匿名对象等; 类和对象相关的概念比较多且关联性很强,综合应用下我们很容易头疼和晕头转向。类和对象确实不太好学,但是这是C++的重点章节,也是基础,我们必须要K.O了它! 加油!


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-01-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 对构造函数的补充
    • 初始化列表
      • 初始化列表格式:
      • 初始化列表特性
      • 初始化列表总结
    • explicit关键字
      • 隐式类型转换
      • 单参数构造函数的隐式类型转换
      • 多参数构造函数的隐式类型转换
  • static修饰类成员
    • 静态成员特性
      • 类静态成员变量的访问方式
      • 对静态成员函数的调用
    • 统计程序中创建的类对象个数
      • 借助全局变量在显式的构造函数和拷贝构造函数内计数;
      • 使用static成员变量在显式的构造函数和拷贝构造函数内计数:
    • 要求对象只能在栈上创建
      • 有关变量生命周期的分析
      • 友元
        • 友元函数
          • 友元函数概念
          • 对友元函数的说明
        • 友元类
          • 友元类特性
      • 内部类
        • 内部类概念
          • 内部类特性
          • 匿名对象
            • 匿名对象概念
              • 匿名对象可能的用途
              • 编译器对拷贝对象的优化
                • 对 单参数或多参数构造函数的隐式类型转换 可能的优化
                  • 对 传参时 可能的优化
                    • 对 函数返回时 可能的优化
                      • 普通优化
                      • 极致优化
                    • 编译器不进行优化的情况
                    • 结语
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档