类和对象(三)

文章目录


一、初始化列表

1.1 引入

我们先来看看下面的代码:

cpp 复制代码
class Date
{
public:
	Date(int year = 2026, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	const int _year;
	const int _month;
	const int _day;
};
int main()
{
	Date d;
	return 0;
}

当我们编译代码时,发现编译器报了一大堆错误。报错的主要原因主要有两个

  1. const变量定义时需要进行初始化
  2. const变量不能作为左值

欸,可能有些小伙伴就纳闷了:我们不是在构造函数中对const成员变量进行初始化了吗? 实际上,在构造函数函数体内进行的并不是初始化,而是赋值操作。因为初始化只能初始化一次,而构造函数体内可以进行多次赋值

出于这个原因,于是编译器就会报出以上两种错误。那怎么办呢?众所周知,初始化是在定义变量时进行的,那变量又是在哪定义的呢?答案是:初始化列表

1.2 概念

在C++中,初始化列表可以认为是成员变量定义的地方

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

举例如下:

cpp 复制代码
class Date
{
public:
	Date(int year = 2026, int month = 1, int day = 1)
		:_year(year)//初始化列表,是每个成员变量定义的地方,可以进行初始化
		,_month(month)//用month的值初始化成员变量_month
		,_day(day)
	{
	}

private:
	//成员变量的声明
	const int _year = 0;
	const int _month = 0;
	const int _day = 0;
};
int main()
{
	Date d;
	return 0;
}

1.3 注意事项

  1. 变量的初始化只能初始化一次,故每个成员变量在初始化列表中只能出现一次
  2. 当类中包含 "引用成员变量、const成员变量、没有默认构造的自定义类型" 成员时,必须放在初始化列表位置进行初始化
cpp 复制代码
class A
{
public:
	A(int a)//显式定义构造函数,不自动生成默认构造函数
		:_a(a)
	{

	}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_a(a) //调用有参构造函数初始化
		,_ref(ref)//初始化引用变量
		,_n(10)//初始化const变量
	{

	}
private:
	A _a;// 没有默认构造函数的类
	int& _ref;// 引用变量
	const int _n;// const变量
};
  1. 建议尽量使用初始化列表初始化,因为初始化列表是成员变量定义的地方,无论你是否显式地写,每个成员都要走初始化列表
cpp 复制代码
class Time
{
public:
	Time(int hour = 0)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date1
{
public:
	Date1(int day)
		:_day(day)  //使用初始化列表进行初始化
		, _t(day)
	{}
private:
	int _day;
	Time _t;
};

class Date2
{
public:
	Date2(int day)
	{
		_day = day; //在构造函数内部进行赋值
		_t = day;
	}
private:
	int _day;
	Time _t;
};
int main()
{
	Date1 d1(3);
	cout << "-----------------------" << endl;
	Date2 d2(3);
	return 0;
}

构造过程分析:

Date1(使用初始化列表):

cpp 复制代码
Date1(int day) : _day(day), _t(day) {}

在构造函数执行前,编译器就知道要用 _t(day) 来初始化 _t

直接调用 Time(int) 构造函数 → 输出1次 "Time()"

Date2(构造函数内赋值):

cpp 复制代码
Date2(int day) {
    _day = day;
    _t = day;
}

两步构造过程:

① 进入构造函数体之前:

编译器发现 _t 没有在初始化列表中指定如何构造

编译器自动调用 Time 的默认构造函数来构造 _t

调用 Time() → 输出第1个 "Time()"

② _t = day; 这行代码实际上是:

cpp 复制代码
_t = Time(day);  // 创建临时对象 + 赋值

创建临时 Time 对象 → 调用 Time(int) → 输出第2个 "Time()"

然后进行赋值操作

关键点:

① 类类型的成员变量在所在类的构造函数执行前就必须被构造

② 如果没有在初始化列表中指定,编译器会自动调用默认构造函数

③ 这就是为什么 Date2 比 Date1 多了一次构造函数调用

  1. C++11支持在声明处给缺省值,这个缺省值就是给初始化列表的。如果初始化列表没有显式给值,则使用这个缺省值;如果显式给了,就用给的值进行初始化
cpp 复制代码
class A
{
public:
	void Print()
	{
		cout << _a << endl;
		cout << _p << endl;
	}
private:
	// 非静态成员变量,可以在成员声明时给缺省值。
	int _a = 10; 
	int* _p = (int*)malloc(4);
	static int _n; //静态成员变量不能给缺省值
};
  1. 初始化列表对成员变量的初始化顺序与其声明的次序相同,与初始化列表的先后次序无关。举个小例子
cpp 复制代码
class A
{
public:
	A(int a)
		:_a1(a)//初始化列表的顺序和声明一样,即也是先初始化_a2再初始化_a1
		,_a2(_a1)//那么,这里用_a1初始化_a2会发生什么?_a1的值是多少
	{

	}

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	//成员变量的声明,先_a2再_a1
	int _a2;
	int _a1;
};
int main()
{
	A aa(1);
	aa.Print();
	return 0;
}

上面代码的输出结果是1 随机值。

解析:由于_a2的声明在_a1前,_a2会先于_a1进行初始化,因此_a2初始化时_a1还是个随机值,故_a2会被初始化为随机值,然后_a1再初始化为1

二、explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有隐式类型转换的作用,如下:

cpp 复制代码
class Date
{
public:
	// 1. 单参构造函数,具有隐式类型转换作用
	Date(int year)
		:_year(year)
	{}

	//2. 虽然有多个参数,但是由于可以只传递1个参数调用,编译器就把它当作"单参构造函数"来处理,支持隐式类型转换
	//用explicit修饰构造函数,可以禁止类型转换
	//explicit Date(int year, int month = 1, int day = 1)
	//: _year(year)
	//, _month(month)
	//, _day(day)
	//{}

	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022); //使用单参构造函数初始化d1

	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2023构造一个匿名的临时对象,最后用这个临时对象给d1对象赋值
	d1 = 2023;
	return 0;
}

隐式类型转换过程:

当执行 d1 = 2023; 时,编译器背后的处理步骤:

① 类型检查:发现左边是 Date 类型,右边是 int 类型

② 寻找转换方法:发现 Date 类有单参构造函数 Date(int year)

③ 创建临时对象:编译器自动用 2023 调用构造函数创建临时 Date 对象

cpp 复制代码
// 编译器自动生成类似代码
Date temp(2023);  // 创建临时对象

④ 调用赋值运算符:将临时对象赋值给 d1

cpp 复制代码
d1.operator=(temp);  // 调用赋值重载

⑤ 销毁临时对象:临时对象生命周期结束

像上面这种运算符左右两边类型不匹配,运算时编译器背后进行处理的过程,称之为隐式类型转换

但是,这样的代码往往可读性不好,我们更希望书写代码时左右两边的类型是一致的,那有没有什么办法可以禁止编译器进行隐式类型转换呢?有,就是explicit关键字。

使用 explicit(显式的) 修饰构造函数,将会禁止构造函数的隐式类型转换。很简单,直接在构造函数前面加上explicit即可

cpp 复制代码
class Date
{
public:
    explicit Date(int year)  // 添加 explicit
        :_year(year)
    {}
    // ... 其他成员
};

int main()
{
    Date d1(2022);  // ✅ 正常构造
    
    // d1 = 2023;   // ❌ 编译错误!不能隐式转换
    
    d1 = Date(2023); // ✅ 必须显式构造
    return 0;
}

三、static成员

3.1 概念

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

一般来说,静态成员变量一定要在类外进行初始化,但在C++11中允许const静态成员变量在类内初始化,

如下所示:

cpp 复制代码
class A
{
	//静态成员函数
	static int GetCount()
	{
		return count;
	}
private:
	static int count;//静态成员变量,必须类外初始化
	const static int num = 10;//const静态成员变量,可以在类内初始化,但不建议
};

int A::count = 10;////静态成员变量要在类外进行初始化

3.2 特性

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须类内声明、类外定义。定义时不用添加static关键字,类中的只是声明
  3. 类的静态成员可以用 类名::静态成员 或者 对象名.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
cpp 复制代码
class Test
{
public:
	static void Fun()
	{
		cout << _a << endl; //error不能访问非静态成员
		cout << _n << endl; //correct
	}
private:
	int _a; //非静态成员
	static int _n; //静态成员
};

小贴士:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量

小问题:静态成员函数可以调用非静态成员函数吗?反过来呢?

问题解答:不行,静态成员函数不能调用非静态成员函数,因为静态成员函数没有隐藏的this指针,而非静态成员函数需要通过this指针来调用。但是非静态成员函数可以调用静态成员函数,因为静态成员函数的特点是没有this指针,故可以直接进行调用

3.3 访问静态成员变量的方法

  1. 当静态成员变量为公有时,有以下几种访问方式:
cpp 复制代码
#include <iostream>
using namespace std;
class Test
{
public:
	static int _n; //公有
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
	Test test;
	cout << test._n << endl; //1.通过类对象突破类域进行访问
	cout << Test()._n << endl; //3.通过匿名对象突破类域进行访问
	cout << Test::_n << endl; //2.通过类名突破类域进行访问
	return 0;
}
  1. 当静态成员变量为私有时,有以下几种访问方式:
cpp 复制代码
#include <iostream>
using namespace std;
class Test
{
public:
	static int GetN()
	{
		return _n;
	}
private:
	static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
int main()
{
	Test test;
	cout << test.GetN() << endl; //1.通过对象调用成员函数进行访问
	cout << Test().GetN() << endl; //2.通过匿名对象调用成员函数进行访问
	cout << Test::GetN() << endl; //3.通过类名调用静态成员函数进行访问
	return 0;
}

静态成员和类的普通成员一样,也有public、private和protected这三种访问级别,所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行访问

四、友元

4.1 概念

在C++中,为了封装性我们一般将成员变量声明为【private】私有的,只允许在类内访问成员变量。但是有时候我们需要在类外访问这些成员变量,此时有两种方法:1.将成员变量声明为【public】共有;2.利用友元

友元提供了一种突破封装的方式,为代码的编写提供了便利。友元分为友元类和友元函数,当一个函数/类声明为某个类的友元函数/类时,这个函数/类访问类中成员时不受访问限定符限制。下面是函数/类声明为友元的方式,用到了friend关键字

cpp 复制代码
class A
{
	friend void GetCount(const A& a);////将全局函数GetCount声明为A类的友元函数
	friend class B;//将B类声明为A类的友元类
private:
	int count = 10;
	int num = 20;
};

class B
{
public:
	void GetNum(const A& a)
	{
		cout<<a.num<<endl://b类中可以访问a类的私有成员
	}
};

void GetCount(const A& a)
{
	cout << a.count << endl;//可以访问A类的私有成员
}

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

小贴士:虽然友元提供了便利,但是友元会增加耦合度,破坏程序的封装性,故不建议使用友元

4.2 友元函数

友元函数一般用作于流提取运算符>>以及流插入运算符<<的重载,这两个运算符的重载比较特殊,不能当做成员函数进行重载

cpp 复制代码
class Date
{
public:
	Date(int year = 2025, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	//如果重载为Date的成员函数,第一个参数为隐藏的this指针,
	// 但cout是ostream类的对象,第一个参数应该是ostream类型,互相矛盾
	// ostream& operator<<(const Date& d); // 调用方式 d << cout;  // 奇怪!不符合习惯
private:
	int _year;
	int _month;
	int _day;
};

//为了让第一个参数类型为ostream,故当做全局函数重载
// 调用方式cout << d;  // 自然、直观
const ostream& operator<<(const ostream& out, const Date& d)
{
	cout << d._year << "年" << d._month << "月" << d._day << "日";
	return out;
}

int main()
{
	Date d;
	cout << d;  //重载流插入运算符使其可以输出日期类 
	return 0;
}

补充知识点:

① ostream来自 C++ 标准库

cpp 复制代码
#include <iostream>  // 这里包含了 ostream 的定义
using namespace std;  // ostream 在 std 命名空间中

在 头文件中,C++ 标准库已经完整定义了 ostream 类:

cpp 复制代码
// 在标准库中大致是这样的(简化说明)
namespace std {
    class ostream {
    public:
        ostream& operator<<(int value);
        ostream& operator<<(double value);
        ostream& operator<<(const char* str);
        // ... 其他重载
    };
    
    // 声明全局的 cout 对象
    extern ostream cout;
}

② << 运算符的返回类型是 ostream&

③ 其中cout是ostream类的一个全局对象,cin是istream类的一个全局变量,<<和>>运算符的重载函数具有返回值是为了实现连续的输入和输出操作

那么问题就来了,既然不能声明为成员函数,那我们在全局函数中要怎么访问Date的私有成员呢?

这时候就不得不使用我们上面说的友元了,将operator<<声明为Date类的友元函数后,代码成功运行:

cpp 复制代码
class Date
{
public:
	Date(int year = 2025, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	friend const ostream& operator<<(const ostream& out, const Date& d);
private:
	int _year;
	int _month;
	int _day;
};

注意事项:

  • 友元函数可以访问类中的私有成员,它是定义在类外部的普通函数,但需要在类的内部进行声明,声明时需要加friend关键字
  • 友元函数不能用const修饰,const只能修饰成员函数
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数

4.3 友元类

友元类中的所有成员函数都可以访问另一个类的非公有成员。

友元关系是单向的,不具有交换性。例如B是A的友元类,B中的所有成员函数可以访问A中的私有成员,但A中的成员函数不能访问B中的私有成员。

举例如下:

cpp 复制代码
class A
{
	friend class B; //定义B是A的友元类
 
	void GetSum(B& b)
	{
		cout << b.sum << endl;  //这里会报错,A类的成员函数无法访问B类的私有成员,不具有交换性
	}
private:
	int count = 20;
};
 
class B
{
	void GetCount(A& a)
	{
		cout << a.count << endl; //通过编译,B是A的友元类,B中成员函数可以访问A的私有成员
	}
private:
	int sum = 10;
};

友元关系也不具有传递性。例如:C是B的友元类,B是A的友元类,无法说明C是A的友元。

举例如下:

cpp 复制代码
class A
{
	friend class B; //定义B是A的友元类
private:
	int a_sum = 10;
};
 
class B
{
	friend class C; //定义C是B的友元类
private:
	int b_sum = 20;
};
 
class C
{
	void GetBSum(B& b)
	{
		cout << b.b_sum << endl;  //编译通过,C是B的友元类
	}
	void GetASum(A& a)
	{
		cout << a.a_sum << endl;  //这里编译器会报错,C不是A的友元类,无法访问私有成员,友元关系不具有传递性
	}
private:
	int c_sum = 30;
};

五、内部类

一个类不仅可以定义在全局范围内,还可以定义在另一个类的内部。我们将定义在某个类内部的类称之为内部类。

下面的B类就是一个内部类:

cpp 复制代码
class A //A称为外部类
{
public:
 
	class B //B类在A类的内部定义,称之为内部类
	{
	private:
		int sum; //b类的成员变量
	};
 
private:
	int count; //a类的成员变量
};

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何特殊的访问权限

有以下两个具体体现

  • 内部类可以定义在外部类的的任何位置,不受外部类访问限定符的限制
  • sizeof(外部类)=外部类,和内部类没有任何关系

内部类是外部类的友元类,内部类可以通过外部类的对象访问外部类的所有成员。但外部类不是内部类的友元类,无权访问内部类的私有成员

cpp 复制代码
class A //A是外部类
{
public:
 
	class B //B是内部类
	{
		int GetACount(A& a)  
		{
			return a.count; //可以访问外部类的私有成员
		}
	private:
		int sum; 
	};
 
	int GetBSum(B& b)
	{
		return b.sum; //这里会报错,外部类不能访问内部类的私有成员
	}
private:
	int count; //a类的成员变量
};

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

cpp 复制代码
class A //A是外部类
{
public:
 
	class B //B是内部类
	{
		int GetACount()  
		{
			return _count; //可以直接访问外部类的静态成员变量,无需类名/类对象
		}
	private:
		int sum; 
	};
 
private:
	static int _count; // A中的静态成员变量
};
 
int A::_count = 10; //类外进行初始化

七、匿名对象

C++支持我们不给对象起名字,这样的对象我们称为匿名对象,其定义方式如下:

cpp 复制代码
int main()
{
	//对象类型+():创建一个匿名对象
	A();  //这里就是创建一个匿名对象A
	return 0;
}

匿名对象的声明周期只在当前行,当前行结束后会自动调用析构函数进行销毁:

cpp 复制代码
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}
	
	int count;
};

int main()
{
	A();
	cout << "程序即将结束" << endl;
	return 0;
}

匿名对象具有常属性,即不能对匿名对象中的成员变量进行修改:

cpp 复制代码
int main()
{
	A().count = 10; //编译器报错:表达式必须是可修改的左值
	return 0;
}

可以给匿名对象取别名,这样可以延长匿名对象的声明周期:

cpp 复制代码
int main()
{
	//给匿名对象取别名
	const A& cla1 = A(); //注意:这里必须是const引用,因为匿名对象具有常性,权限不能放大
	cout << "程序即将结束" << endl;
	return 0;
}

匿名对象经常用在仅需调用某个类的成员函数的情况,可以简化我们代码的编写。举例如下

cpp 复制代码
class Solution //Solution类用来求两数之和
{
public:
	int Sum_Solution(int x,int y)  //返回两数之和 
	{
		return x + y;
	}
};
 
int main()
{
	//不使用匿名对象
	Solution s1; //要先定义一个类对象,这个对象仅仅只是用来调用方法
	s1.Sum_Solution(2, 2); //然后再去调用成员函数
 
	//使用匿名对象
	Solution().Sum_Solution(2, 3); //代码更加简便
}

上面的Solution类是不是很熟悉?没错,在我们使用C++进行刷题时每次能够遇到它

注意:

cpp 复制代码
// 创建命名对象
A a;           // 调用默认构造函数
A a();         // 函数声明!不是对象创建(最令人烦恼的解析)

// 创建临时对象
A();           // 明确调用构造函数创建临时对象

// 动态创建
new A();       // 调用构造函数在堆上创建对象
相关推荐
lilv661 小时前
visual studio 2026中C4996错误 ‘operator <<‘: 被声明为已否决
c++·ide·visual studio
谁刺我心1 小时前
蓝桥杯C++常用STL
c++·算法·蓝桥杯
Demon--hx1 小时前
[C++]迭代器失效问题
前端·c++
liulilittle1 小时前
C++ 计算当前时区偏移量秒数(GMT/UNIX偏移量)
linux·c++·unix
再睡一夏就好1 小时前
深入理解Linux程序加载:从ELF文件到进程地址空间的完整旅程
linux·运维·服务器·c++·学习·elf
lijiatu100861 小时前
[C++] 上锁、解锁、获取锁、释放锁的区别
开发语言·c++
阿沁QWQ1 小时前
STL和string实现
开发语言·c++
乌萨奇也要立志学C++2 小时前
【Linux】线程概念 线程与进程深度剖析:虚实内存转换、实现机制与优缺点详解
linux·c++
爱学习的小邓同学2 小时前
数据结构 --- 二叉搜索树
数据结构·c++