C++ 类与对象易错点:初始化列表顺序 / 静态成员访问 / 隐式类型转换

欢迎来到 s a y − f a l l 的文章 欢迎来到say-fall的文章 欢迎来到say−fall的文章

🌈 say-fall:个人主页 🚀 专栏:《手把手教你学会C++》 | 《C语言从零开始到精通》 | 《数据结构与算法》 | 《小游戏与项目》 💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。


前言:

输入内容


文章目录


正文:

一、构造函数内的初始化

之前我们在初始化成员变量时候都是在构造函数内部赋值初始化,而还有一种初始化方法是初始化列表

类里面的成员变量int _hour;可以看作是成员变量的声明,而初始化列表就是成员变量的定义。

cpp 复制代码
class Time
{
public:
 Time(int hour)
 :_hour(hour)
 {
 cout << "Time()" << endl;
 }
private:
 int _hour;
};

这里的 :_hour(hour)就是初始化列表,如果有多项成员变量的话,要用,隔开。所谓的中间就是在()和{}。

需要注意的是,有一些成员函数可以不在初始化列表里写(隐式);有一些成员函数是必须在初始化列表中初始化(显式):const修饰的变量、引用、没有默认构造的自定义类型

cpp 复制代码
class Date
{
public:
	Date(int& xx,int year, int month, int day)
	//成员变量的定义(开空间)
	//1. 初始化列表
		:_year(year)
		,_month(month)
		,_day(day)
		//必须初始化的
		,_n(1)
		,_ref(xx)
		,_t(1)
	//2. 函数体内赋值
	{}
private:
	//成员变量的声明
	int _year;
	int _month;
	int _day;

	//必须在初始化列表中初始化
	const int _n;//const修饰变量
	int& _ref;//引用
	Time _t;//没有默认构造的自定义类型
};

对于隐式和显式的初始化列表,程序执行时有如下逻辑:

cpp 复制代码
1.初始化列表是成员变量【定义】的地方,一定会执行(哪怕自己没写,编译器也会生成空的初始化列表执行)
2.显式初始化:使用显式的初始化(就是写了初始化列表)
  (形参缺省值的作用就是给显式的初始化使用的)
3.没有显式初始化:有缺省值:使用声明的地方的缺省值(声明处,而非形参)
				 没有缺省值:内置类型:不确定是否初始化,取决于编译器(随机值/野值)
							自定义类型:调用自身的默认构造函数,没有默认构造的话直接编译报错
4.有三种成员【必须在初始化列表显式初始化】(缺一不可,写在函数体赋值编译报错):
					  const int _n;//const修饰的成员变量(只读,定义时必须赋值)
					  int& _ref;//引用成员变量(引用必须在定义时绑定对象,不可后期赋值)
					  Time _t;//无默认构造的自定义类型成员(编译器无法自动初始化)
5.初始化列表是按照【类内成员的声明顺序】初始化的,和初始化列表的书写顺序无关

二、类型转换

  • C++⽀持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
  • 构造函数前⾯加explicit就不再⽀持隐式类型转换。
  • 类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持。
cpp 复制代码
#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)
 		:_b(a.Get())
 	{}
private:
 	int _b = 0;
};
int main()
{
 	A aa1 = 1;// 用1构造⼀个A的临时对象,再用这个临时对象拷⻉构造aa1(包含隐式类型转换)
 	// 编译器遇到连续构造+拷⻉构造->优化为直接构造 
 	aa1.Print();
 	const A& aa2 = 1;
 	// C++11之后才⽀持多参数转化 
 	A aa3 = { 2,2 };
 	// aa3隐式类型转换为b对象 
 	// 原理跟上⾯类似 
 	B b = aa3;
 	const B& rb = aa3;
 	return 0;
}

三、static成员

  • ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。
  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区
  • 突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量 静态成员函数
  • 静态成员也是类的成员,受 publicprotectedprivate 访问限定符的限制。
  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不⾛构造函数初始化列表。

先来看一下静态成员变量

cpp 复制代码
class A
{
public:
	//构造函数
	A()
	{
		++_scount;
	}
	//拷贝构造
	A(const A& t)
	{
		++_scount;
	}
	//析构函数
	~A()
	{
		--_scount;
	}
private:
	// 类里面声明 
	static int _scount;
};
// 类外面初始化 
int A::_scount = 0;
  • ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
  • 静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
  • ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

下面看一下静态成员函数:

cpp 复制代码
class A
{
public:
	//构造函数
	A()
	{
		++_scount;
	}
	//拷贝构造
	A(const A& t)
	{
		++_scount;
	}
	//析构函数
	~A()
	{
		--_scount;
	}
	//静态成员函数,没有this指针
	static int GetACount()
	{
		return _scount;
	}
private:
	// 类里面声明 
	static int _scount;
};
// 类外面初始化 
int A::_scount = 0;

这里面的GetACount()就是静态成员函数

这里主要是掌握静态成员函数的两种用法:
A::GetACount()a1.GetACount()(不推荐后一种)

还有一种是对象指针->GetACount(和a1.GetACount()一样不推荐)

cpp 复制代码
int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	A* p = &a1;
	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;
	cout << p->GetACount() << endl;
	//编译报错:error C2248: "A::_scount": ⽆法访问 private 成员(在"A"类中声明) 
	//cout << A::_scount << endl;
	return 0;
}
  • 静态成员函数 和 非静态成员函数 对比表
对比维度 静态成员函数(static 修饰) 非静态成员函数(普通成员函数)
所属对象 属于类本身,所有对象共享同一份函数拷贝 属于具体对象,每个对象都隐含绑定该函数的调用权限
this 指针 无 this 指针(不绑定任何对象) 有隐含的 this 指针(指向调用该函数的对象)
调用方式 1. 推荐:类名::函数名()(如 A::Print()) 2. 兼容:对象.函数名()(不推荐,语义混乱) 必须:对象.函数名()对象指针->函数名()(如 a.Print()/p->Print()
访问成员权限 只能访问类的静态成员(静态变量/静态函数),无法访问非静态成员(无this指针) 可访问类的所有成员(静态+非静态),通过this指针访问非静态成员
生命周期 程序启动时创建,程序结束时销毁(和类的生命周期一致) 随对象的创建/销毁而绑定/解绑(函数代码本身只有一份,调用时绑定this)
内存存储位置 存储在全局/静态存储区(不和对象实例绑定) 函数代码存储在代码段,调用时通过this指针关联对象

四、友元

  • 友元提供了⼀种突破类访问限定符封装 的⽅式,友元分为:友元函数友元类 ,在函数声明或者类声明的前⾯加friend,并且把友元声明放到⼀个类的⾥⾯。
  • 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。

关于友元类:

  • 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
  • 友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
  • 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。

关于友元函数:

  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
  • 友元函数可以在类定义的任何地⽅声明,不受类访问限定符限制。
  • ⼀个函数可以是多个类的友元函数。

五、内部类

  • 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
  • 内部类默认是外部类的友元类。
  • 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地⽅都⽤不了。

六、匿名对象

  • 类型(实参) 定义出来的对象叫做匿名对象,相⽐之前我们定义的 类型 对象名(实参) 定义出来的叫有名对象
  • 匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。
cpp 复制代码
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;
}

七、对象拷贝时编译器的优化

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

下面提供一些代码用来观察编译器的优化:

cpp 复制代码
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;
	}
	void Print()
	{
		cout << "Print()->" << _a1 << endl;
	}
	A& operator++()
	{
		++_a1;
		return *this;
	}
private:
	int _a1 = 1;
};
void f1(A aa)//传值传参,存在拷贝构造
{
}
A f2()
{
	A aa(1);
	//测试优化会不会有错误
	++aa;
	return aa;
}

int main()
{
	//优化了,有类型转换:构造+拷贝构造->直接构造
	A aa1 = 1;
	const A& aa2 = 1;
	cout << endl;

	//没有优化:构造+拷贝构造
	A aa3(1);
	f1(aa3);
	cout << endl;

	//优化,匿名对象:构造+拷贝构造
	f1(A(1));
	cout << endl;

	//优化,直接类型转换
	f1(1);
	cout << endl;

	//优化, 不生成【拷贝的临时对象】,直接在返回值位置构造aa,省略1次拷贝构造
	f2().Print();
	cout << "************" << endl << endl;

	//优化,不生成临时对象
	A ret = f2();
	ret.Print();
	cout << "************" << endl << endl;
	return 0;
}

  • 本节完...
相关推荐
热爱专研AI的学妹16 小时前
2026世界杯观赛工具自制指南:实时比分推送机器人搭建思路
开发语言·人工智能·python·业界资讯
Dev7z16 小时前
基于MATLAB图像处理的苹果品质自动分级系统设计与实现
开发语言·图像处理·matlab
落羽凉笙16 小时前
Python基础(4)| 详解程序选择结构:单分支、双分支与多分支逻辑(附代码)
android·服务器·python
源代码•宸17 小时前
Golang基础语法(go语言指针、go语言方法、go语言接口、go语言断言)
开发语言·经验分享·后端·golang·接口·指针·方法
Bony-17 小时前
Golang 常用工具
开发语言·后端·golang
Paul_092017 小时前
golang编程题
开发语言·算法·golang
携欢17 小时前
portswigger靶场之修改序列化数据类型通关秘籍
android·前端·网络·安全
csbysj202017 小时前
Go 语言变量作用域
开发语言
pyniu17 小时前
Spring Boot车辆管理系统实战开发
java·spring boot·后端