类与对象(三)

再谈构造函数

构造函数体赋值

在创建对象时,编译器会通过调用构造函数,给对象中的各个成员变量一个合适的初始值:

调用该构造函数后,对象中的每个成员变量都有了一个初始值 ,但是构造函数中的语句只能将其称作为赋初值,而不能称作为初始化 。因为初始化只能初始化一次 ,而构造函数体内可以进行多次赋值。

cpp 复制代码
class Date
{
public:
	// 构造函数
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;// 第一次赋值
		_year = 2022;// 第二次赋值
		//...
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

初始化列表

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

cpp 复制代码
class Date
{
public:
	// 构造函数
	Date(int year = 0, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

注意事项:
一、每个成员变量在初始化列表中只能出现一次

因为初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现。
二、类中包含以下成员,必须放在初始化列表进行初始化:

在定义时就必须进行初始化的变量类型,就必须放在初始化列表进行初始化。
1.引用成员变量

引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化

cpp 复制代码
	int a = 10;
	int& b = a;// 创建时就初始化

2.const成员变量

被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。

cpp 复制代码
    const int a = 10;//correct 创建时就初始化
    const int b;//error 创建时未初始化

3.自定义类型成员(该类没有默认构造函数)

若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。

在这里再声明一下,默认构造函数是指不用传参就可以调用的构造函数:

1.我们不写,编译器自动生成的构造函数。

2.无参的构造函数。

3.全缺省的构造函数。

cpp 复制代码
class A //该类没有默认构造函数 
{
public:
	A(int val) //注:这个不叫默认构造函数(需要传参调用)
	{
		_val = val;
	}
private:
	int _val;
};

class B
{
public:
	B()
		:_a(2021) //必须使用初始化列表对其进行初始化
	{}
private:
	A _a; //自定义类型成员(该类没有默认构造函数)
};

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

因为初始化列表实际上就是当你实例化一个对象时,该对象的成员变量定义的地方,所以无论你是否使用初始化列表,都会走这么一个过程(成员变量需要定义出来)。

严格来说:
1.对于内置类型,使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,其差别就类似于如下代码:

cpp 复制代码
// 使用初始化列表
int a = 10
// 在构造函数体内初始化(不使用初始化列表)
int a;
a = 10;

2.对于自定义类型,使用初始化列表可以提高代码的效率

cpp 复制代码
class Time
{
public:
	Time(int hour = 0)
	{
		_hour = hour;
	}
private:
	int _hour;
};
class Test
{
public:
	// 使用初始化列表
	Test(int hour)
		:_t(12)// 调用一次Time类的构造函数
	{}
private:
	Time _t;
};

对于以上代码,当我们要实例化一个Test类的对象时,我们使用了初始化列表,在实例化过程中只调用了一次Time类的构造函数。

我们若是想在不使用初始化列表的情况下,达到我们想要的效果,就不得不这样写了:

这时,当我们要实例化一个Test类的对象时,在实例化过程中会先在初始化列表时调用一次Time类的构造函数,然后在实例化t对象时调用一次Time类的构造函数,最后还需要调用了一次Time类的赋值运算符重载函数,效率就降下来了。

cpp 复制代码
class Time
{
public:
	Time(int hour = 0)
	{
		_hour = hour;
	}
private:
	int _hour;
};
class Test
{
public:
	// 在构造函数体内初始化(不使用初始化列表)
	Test(int hour)
	{ //初始化列表调用一次Time类的构造函数(不使用初始化列表但也会走这个过程)
		Time t(hour);// 调用一次Time类的构造函数
		_t = t;// 调用一次Time类的赋值运算符重载函数
	}
private:
	Time _t;
};

四、成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关

举个例子:

cpp 复制代码
#include <iostream>
using namespace std;
int i = 0;
class Test
{
public:
	Test()
		:_b(i++)
		,_a(i++)
	{}
	void Print()
	{
		cout << "_a:" << _a << endl;
		cout << "_b:" << _b << endl;
	}
private:
	int _a;
	int _b;
};
int main()
{
	Test test;
	test.Print(); //打印结果test._a为0,test._b为1
	return 0;
}

explicit关键字

构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还支持隐式类型转换

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 0) //单个参数的构造函数
		:_year(year)
	{}
	void Print()
	{
		cout << _year << endl;
	}
private:
	int _year;
};
int main()
{
	Date d1 = 2021; //支持该操作
	d1.Print();
	return 0;
}

在语法上,代码中Date d1 = 2021等价于以下两句代码:

cpp 复制代码
Date tmp(2021); //先构造
Date d1(tmp); //再拷贝构造

Date d = 2021;这个形式看起来像"用2021初始化d",但实际上编译器要:

  1. 用2021创建一个临时Date对象

  2. 用这个临时对象拷贝构造d

  3. 编译器优化为直接构造d(2021)

explicit阻止的就是第一步的自动创建临时对象

这就叫做隐式类型转换。

对于单参数的自定义类型来说,Date d1 = 2021这种代码的可读性不是很好,我们若是想禁止单参数构造函数的隐式转换,可以用关键字explicit来修饰构造函数

static成员

概念

声明为static的类成员称为类的静态成员 。用static修饰的成员变量,称之为静态成员变量 ;用static修饰的成员函数,称之为静态成员函数静态成员变量一定要在类外进行初始化。

特性
一、静态成员为所有类对象所共享,不属于某个具体的对象

cpp 复制代码
#include <iostream>
using namespace std;
class Test
{
private:
	static int _n;
};
int main()
{
	cout << sizeof(Test) << endl;
	return 0;
}

结果计算Test类的大小为1,因为静态成员_n是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和。

二、静态成员变量必须在类外定义,定义时不添加static关键字

cpp 复制代码
class Test
{
private:
	static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;

三、静态成员函数没有隐藏的this指针,不能访问任何非静态成员

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

cpp 复制代码
class Test
{
public:
	static void Fun()
	{
		cout << _a << endl; //error不能访问非静态成员
		cout << _n << endl; //correct
	}
private:
	int _a; //非静态成员
	static int _n; //静态成员
};

四、访问静态成员变量的方法
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;
}

2.当静态成员变量为私有时,有以下几种访问方式:

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时,尽管我们突破了类域,也不能对其进行访问。

注意区分两个问题:
 1、静态成员函数可以调用非静态成员函数吗?
 2、非静态成员函数可以调用静态成员函数吗?

问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。

问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制。

成员初始化的新玩法

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; //静态成员变量不能给缺省值
};

友元

友元分为友元函数和友元类 。友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元函数

友元函数可以直接访问类的私有成员 ,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明 ,声明时需要加friend关键字。

之前实现的日期类,我们现在尝试重载operator<<,但是我们发现没办法将其重载为成员函数,因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置:this指针默认是第一个参数,即左操作数,但是实际使用中cout需要是第一个形参对象才能正常使用。

所以我们要将operator<<重载为全局函数,但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。(operator>>同理)

我们都知道C++的<<和>>很神奇,因为它们能够自动识别输入和输出变量的类型, 我们使用它们时不必像C语言一样增加数据格式的控制。实际上,这一点也不神奇,内置类型的对象能直接使用cout和cin输入输出,是因为库里面已经将它们的<<和>>重载好了 ,<<和>>能够自动识别类型,是因为它们之间构成了函数重载。

所以,我们若是想让<<和>>也自动识别我们的日期类,就需要我们自己写出对应的运算符重载函数。

cpp 复制代码
class Date
{
	// 友元函数的声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
// <<运算符重载
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day<< endl;
	return out;
}
// >>运算符重载
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

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

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

友元类

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

cpp 复制代码
class A
{
    // 声明B是A的友元类
    friend class B;
public:
    A(int n = 0)
        :_n(n)
    {}
private:
    int _n;
};
class B
{
public:
    void Test(A& a)
    {
        // B类可以直接访问A类中的私有成员变量
        cout << a._n << endl;
    }
};

友元类说明:
1、友元关系是单向的,不具有交换性。

例如上述代码中,B是A的友元,所以在B类中可以直接访问A类的私有成员变量,但是在A类中不能访问B类中的私有成员变量。
2、友元关系不能传递。

如果A是B的友元,B是C的友元,不能推出A是C的友元。

内部类

概念

概念:如果一个类定义在另一个类的内部,则这个类被称为内部类。

注意:

1.此时的内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象区调用内部类

2、外部类对内部类没有任何优越的访问权限。

3、内部类就是外部类的友元类 ,即内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

特性

1、内部类可以定义在外部类的public、private以及protected这三个区域中的任一区域。

2、内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。

3、外部类的大小与内部类的大小无关

cpp 复制代码
#include <iostream>
using namespace std;
class A //外部类
{
public:
	class B //内部类
	{
	private:
		int _b;
	};
private:
	int _a;
};
int main()
{
	cout << sizeof(A) << endl; //外部类的大小
	return 0;
}

外部类A的大小为4,与内部类的大小无关。

匿名对象

1. 什么是匿名对象?

匿名对象是指在创建对象时不给对象命名,直接通过构造函数创建的对象。

2. 匿名对象的特点:

复制代码
A();  // 这是一个匿名对象
  • 没有对象名:不需要变量名来引用

  • 生命周期短暂:只存在于创建它的那一行语句

  • 自动析构:语句执行完毕后立即调用析构函数

3. 使用场景示例:

cpp 复制代码
// 1. 直接调用函数
Solution().Sum_Solution(10);  // 创建匿名对象调用成员函数

// 2. 作为函数参数传递
void func(A a) { ... }
func(A());  // 传递匿名对象

// 3. 测试构造函数
A(1);  // 测试带参数的构造函数

4. 注意与函数声明的区别:

cpp 复制代码
A aa1();  // 这不是匿名对象,这是函数声明!
           // 编译器认为这是声明一个返回A类型、无参数的函数aa1
A aa1;     //  这是默认构造的对象
A();       //  这是匿名对象

二、编译器优化

1. 常见的编译器优化场景:

场景1:传值传参
cpp 复制代码
void f1(A aa) { ... }
A aa1;
f1(aa1);  // 会调用拷贝构造函数

输出A(const A& aa)

场景2:传值返回
复制代码
A f2() {
    A aa;
    return aa;  // 可能触发RVO(返回值优化)
}
f2();
场景3:隐式类型转换优化
复制代码
f1(1);  // 1会隐式转换为A对象

优化前

  1. 用1构造一个临时A对象

  2. 用临时对象拷贝构造形参

  3. 销毁临时对象

优化后

  • 直接构造形参

  • 输出A(int a)

场景4:显式构造临时对象
复制代码
f1(A(2));

优化前

  1. 构造A(2)临时对象

  2. 用临时对象拷贝构造形参

  3. 销毁临时对象

优化后

  • 直接构造形参

  • 输出A(int a)

场景5:连续拷贝构造优化
复制代码
A aa2 = f2();

优化前

  1. f2()中构造局部对象

  2. 拷贝构造返回的临时对象

  3. 用临时对象拷贝构造aa2

  4. 销毁临时对象

优化后

  • 直接在f2()中构造aa2

  • 输出A(int a)

场景6:无法优化的场景
复制代码
aa1 = f2();  // 已有对象aa1,接收f2()的返回值

无优化

  1. f2()中构造局部对象

  2. 拷贝构造返回的临时对象

  3. 调用赋值运算符

  4. 销毁临时对象

  5. 销毁f2()中的局部对象

输出

复制代码
A(int a)              // f2中构造局部对象
A(const A& aa)       // 构造返回的临时对象
A& operator=(const A& aa)  // 赋值给aa1
~A()                  // 析构临时对象
~A()                  // 析构f2中的局部对象

2. 优化原理:

  • RVO(Return Value Optimization):返回值优化

  • NRVO(Named Return Value Optimization):具名返回值优化

  • C++17强制要求的部分优化:在某些情况下,编译器必须进行优化

3. 实际应用建议:

  1. 尽量使用匿名对象

    复制代码
    // 简洁高效
    Solution().Solve(problem);
    
    // 替代
    Solution s;
    s.Solve(problem);
  2. 利用编译器优化

    复制代码
    // 返回局部对象时,信任编译器优化
    vector<int> getData() {
        vector<int> data = {1, 2, 3};
        return data;  // 编译器会优化
    }
  3. 避免不必要的拷贝

    复制代码
    // 使用const引用接收临时对象
    void process(const A& obj) { ... }
    
    // 传值会触发拷贝
    void process(A obj) { ... }  // 可能产生额外拷贝

4. 验证优化效果:

可以通过在构造函数、拷贝构造函数、析构函数中打印信息来观察优化情况:

复制代码
class Test {
public:
    Test() { cout << "Constructor" << endl; }
    Test(const Test&) { cout << "Copy Constructor" << endl; }
    ~Test() { cout << "Destructor" << endl; }
};

总结:

  1. 匿名对象是临时对象,生命周期短,适合一次性使用

  2. 编译器优化能减少不必要的拷贝,提高效率

  3. 理解这些优化有助于编写更高效的代码

  4. 在C++17及更高版本中,某些优化是强制性的

这些特性是C++性能优化的重要组成部分,理解它们有助于写出更高效的C++代码。

再次理解类和对象

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现

实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创

建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

  1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什

么属性,有那些功能,即对洗衣机进行抽象认知的一个过程

  1. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清

楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、

Java、Python等)将洗衣机用类来进行描述,并输入到计算机中

  1. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣

机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才

能洗衣机是什么东西。

  1. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的描述该对象具有那
些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化
具体的对象。

相关推荐
️是781 小时前
信息奥赛一本通—编程启蒙(3395:练68.3 车牌问题)
数据结构·c++·算法
计算机安禾2 小时前
【c++面向对象编程】第24篇:类型转换运算符:自定义隐式转换与explicit
java·c++·算法
雪度娃娃2 小时前
转向现代C++——优先选用nullptr而不是0和NULL
开发语言·c++
我星期八休息2 小时前
Linux系统编程—基础IO
linux·运维·服务器·c语言·c++·人工智能·算法
萌新小码农‍2 小时前
python装饰器
开发语言·前端·python
KK溜了溜了2 小时前
Python从入门到精通
服务器·开发语言·python
故事和你913 小时前
洛谷-【图论2-1】树5
开发语言·数据结构·c++·算法·动态规划·图论
threelab3 小时前
Three.js 初中数学函数可视化 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器
xiaoshuaishuai83 小时前
C# CDN加速与离线包优化PowerSetting慢问题
开发语言·windows·spring·c#