C++从入门到入土 (4):类和对象(下)

一、再探构造函数

初始化列表

之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个”成员变量”后面跟一个放在括号中的初始值或表达式。

//初始化列表
Test(int a,int b)
	:_a(a)
	,_b(b)
{}
//函数体内赋值
Test(int a, int b)
{
	_a = a;
	_b = b;
}
  1. 每个成员变量初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
  2. 引用成员变量,const成员变量,没有默认构造的类类型变量(比如之前的使用栈类类型实现队列类类型,队列类类型构造要用栈类类型的构造),必须放在初始化列表位置进行初始化,否则会编译报错。
  3. C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
private:
	int _a = 1;
	int _b = 1;
  1. 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
  2. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。比如以下代码:
public:
	Test(int a,int b)
		:_a(a)
		,_b(_a)
	{}

	void Print()
	{
		cout << _a << _b << endl;
	}
	
private:
	int _b = 1;
	int _a = 1;

这里输出的是1和随机值,因为_b是先声明的,所以先初始化,此时_a还没有初始化,所以是随机值。

总结:
无论是否显示写初始化列表,每个构造函数都有初始化列表。
无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化;
在这里插入图片描述

二、类型转换

  1. C++支持内置类型,隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
  2. 构造函数前面加explicit就不再支持隐式类型转换。
  3. 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
    以下代码解释:
#include<iostream>
using namespace std;

class A
{
public:
    // 无explicit:支持int隐式转换为A对象
    A(int a1)
        :_a1(a1)  // 初始化列表:用传入的a1覆盖默认值1
    {}

    A(int a1, int a2)
        :_a1(a1), _a2(a2)  // 用传入的a1/a2覆盖默认值
    {}

    void Print()
    {
        cout << _a1 << " " << _a2 << endl;
    }

    int Get() const
    {
        return _a1 + _a2;
    }

private:
    int _a1 = 1;
    int _a2 = 2;
};

class B
{
public:
    // 支持A对象隐式转换为B对象(构造函数参数为const A&,避免拷贝)
    B(const A& a)
        :_b(a.Get())  // 用A对象的Get()返回值初始化_b
    {}

private:
    int _b = 0;
};

int main()
{
    // 1. 内置类型int隐式转换为A对象
    // 编译器优化:A aa1 = 1; 等价于 A aa1(1);(而非先构造临时对象再拷贝)
    //vs自动优化的,有的编译器不会优化
    A aa1 = 1;
    aa1.Print();  // 输出:1 2(_a1=1,_a2保留默认值2)

    // 2. const引用绑定隐式转换的临时A对象(临时对象生命周期延长至引用生命周期)
    const A& aa2 = 1;

    // 3. C++11后支持多参数隐式转换:{2,2}隐式转换为A对象
    A aa3 = { 2,2 };
    aa3.Print();  // 输出:2 2

    // 4. 类对象A隐式转换为类对象B
    B b = aa3;  // 用aa3(A对象)构造B对象,编译器优化为直接构造
    const B& rb = aa3;  // const引用绑定隐式转换的临时B对象

    return 0;
}

隐式类型转换(内置→类):
当用A aa1 = 1;时,编译器会自动调用A(int a1)构造函数,把int类型的1转换成A对象(前提是构造函数没有explicit)。如果给A(int a1)加explicit,则A aa1 = 1;会编译报错,必须显式写A aa1(1);。
C++11 多参数隐式转换:
旧版 C++ 只支持单参数构造函数的隐式转换,C++11 后支持A aa3 = {2,2};这种多参数的列表初始化隐式转换(对应A(int a1, int a2)构造函数)。
类对象间的隐式转换:
B b = aa3;能执行,是因为B有以const A&为参数的构造函数,编译器会自动把A对象aa3转换成B对象。
const 引用绑定临时对象:
const A& aa2 = 1;和const B& rb = aa3;能编译通过,是因为const引用可以延长临时对象的生命周期(非 const 引用无法绑定临时对象)。

三、static成员

静态成员变量

定义:用static修饰的类成员变量,属于整个类而非某个具体对象。
特点:

  1. 存放在静态区,不占用对象的内存空间,所有对象共享同一份值。
  2. 必须在类外初始化(格式:类型 类名::静态变量名 = 初始值;),类内仅声明;不能在类内声明时给缺省值(因为缺省值属于构造函数初始化列表,而静态变量不依赖对象、不走构造函数)。
  3. 受public/protected/private限制(private 的静态变量无法在类外直接访问)。
  4. **访问方式:**类名::静态变量名 或 对象名.静态变量名(突破类域,但受访问权限限制)。

静态成员函数

定义:用static修饰的类成员函数,无this指针(因为不依赖具体对象)。
特点:

  1. 只能访问类的静态成员(静态变量 / 静态函数),无法访问非静态成员(非静态成员需要this指针指向具体对象);
  2. 非静态成员函数可以任意访问静态成员(有this指针,且静态成员属于类)。
  3. **访问方式:**类名::静态函数名() 或 对象名.静态函数名()(同样受访问权限限制)。

代码解释

下面的代码展现了静态成员函数的基本应用之一。

#include<iostream>
using namespace std;

class A
{
public:
    // 构造函数:创建对象时,计数+1
    A()
    {
        ++_scount;
    }

    // 拷贝构造函数:拷贝创建对象时,计数+1
    A(const A& t)
    {
        ++_scount;
    }

    // 析构函数:销毁对象时,计数-1
    ~A()
    {
        --_scount;
    }

    // 静态成员函数:获取当前对象个数(无this指针,只能访问静态变量_scount)
    static int GetACount()
    {
        return _scount;
    }

private:
    // 类内声明静态成员变量:用于统计对象个数
    static int _scount;
};

// 类外初始化静态成员变量(必须!初始值为0)
int A::_scount = 0;

int main()
{
    // 初始时无对象,计数为0
    cout << A::GetACount() << endl;  // 输出:0

    // 创建2个普通对象,构造函数触发,计数+2 → 计数=2
    A a1, a2;
    // 拷贝构造创建a3,计数+1 → 计数=3
    A a3(a1);

    // 类名访问静态函数,输出当前计数:3
    cout << A::GetACount() << endl;
    // 对象访问静态函数,输出当前计数:3(和类名访问结果一致)
    cout << a1.GetACount() << endl;

    // 错误:_scount是private,类外无法直接访问(即使通过类名)
    // cout << A::_scount << endl;

    return 0;
}

这里有一道题,求1+2+3+…+n,但是不能用循环,递归等,只能用类来实现,该怎么办呢?
我们可以通过访问n次类构造函数来实现。

#include <iostream>
using namespace std;

class Sum
{
public:
    // 构造函数:每次创建Sum对象时自动执行
    Sum()
    {
        _ret += _i;  // 把当前_i的值累加到_ret中
        ++_i;        // _i自增,为下一个对象的累加做准备
    }

    // 静态成员函数:获取最终的累加结果(无this指针,只能访问静态成员)
    static int GetRet()
    {
        return _ret;
    }

private:
    static int _i;   // 静态变量:记录当前要累加的数字(初始为1)
    static int _ret; // 静态变量:记录累加的总和(初始为0)
};

// 静态成员变量类外初始化
int Sum::_i = 1;
int Sum::_ret = 0;

class Solution 
{
public:
    int Sum_Solution(int n) {
        // 创建一个长度为n的Sum类型数组arr
        // 数组有n个元素,就会调用n次Sum的构造函数
        // 注意:vs不支持变长数组
        Sum arr[n];
        // 调用静态函数获取累加结果
        return Sum::GetRet();
    }
};

// 测试代码
int main()
{
    Solution s;
    cout << s.Sum_Solution(5) << endl; // 输出15(1+2+3+4+5)
    return 0;
}

四、友元

我们常常遇到一种情况,我们在类外的函数需要用到类内私有或收到保护的对象,而使用这些对象需要是类成员函数,但如果是类成员函数就有我们不需要的隐式的this指针。该怎么办呢?
我们可以把类外的函数的声明放到类里面,并且在前面加上friend,这样类外的函数就可以使用类内私有的受保护的对象,并且没有this指针。这就是友元函数。
特点:

  1. 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。
  2. 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
  4. 一个函数可以是多个类的友元函数。
  5. 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
  6. 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
  7. 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
  8. 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
// 前置声明,否则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;
}

五、内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象不包含内部类
特点:

  1. 内部类默认是外部类的友元类。
  2. 内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。

我们在static成员中提到的1+2+…+n那道题还可以做出优化,可以把Sum类放进Solution类里面,Sum类就成为了内部类,做出了限制。
代码如下:


class Solution 
{
	// 内部类
	class Sum
	{
	public:
		Sum()
		{
			_ret += _i;
			++_i;
		}
	};
	
	static int _i;
	static int _ret;
public:
	int Sum_Solution(int n)
	{
		// 变⻓数组
		Sum arr[n];
		return _ret;
	}
};
int Solution::_i = 1;
int Solution::_ret = 0;

六、匿名对象

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

int main()
{
	// 有名对象
	A aa1(1);

	// 转换产生的临时对象
	const A& aa2 = 1;

	// 匿名对象
	A(2);
	A();

	const A& aa3 = A(3);

	Solution s;
	s.Sum_Solution(10);

    //使用匿名对象使用类成员函数
	Solution().Sum_Solution(10);

	return 0;
}

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

C++标准允许编译器在特定场景下省略不必要的拷贝/移动构造(即使拷贝/移动构造有副作用,比如你代码里的打印语句),核心目的是减少临时对象的创建和拷贝开销,主要分为两类:

  1. 复制省略(Copy Elision):直接省略拷贝构造 / 移动构造,用 “直接构造” 替代 “构造 + 拷贝”;
    2.** 返回值优化(RVO/NRVO)**:针对函数返回值的优化,分为 “具名返回值优化(NRVO,返回命名对象)” 和 “无名返回值优化(RVO,返回临时对象)”。

// 定义类A,自定义构造、拷贝构造、赋值重载、析构函数
// 每个函数都加打印,方便观察编译器是否调用(核心用于验证优化)
class A
{
public:
    // 带默认参数的构造函数
    A(int a = 0)
        :_a1(a)  // 初始化列表
    {
        cout << "A(int a):构造函数调用,_a1 = " << _a1 << endl;
    }

    // 拷贝构造函数
    // 用已有A对象初始化新对象时调用
    A(const A& aa)
        :_a1(aa._a1)  // 拷贝已有对象的_a1值
    {
        cout << "A(const A& aa):拷贝构造函数调用,_a1 = " << _a1 << endl;
    }

    // 赋值运算符重载
    // 给已有对象赋值时调用(区别于拷贝构造:拷贝构造是创建新对象,赋值是修改已有对象)
    A& operator=(const A& aa)
    {
        cout << "A& operator=(const A& aa):赋值重载调用" << endl;
        // 防止自赋值(比如aa1 = aa1)
        if (this != &aa)
        {
            _a1 = aa._a1;  // 拷贝值
        }
        return *this;  // 返回自身,支持链式赋值(如aa1 = aa2 = aa3)
    }

    // 析构函数
    ~A()
    {
        cout << "~A():析构函数调用,_a1 = " << _a1 << endl;
    }

private:
    int _a1 = 1;
};

// 场景1:传值传参函数(值传递会拷贝实参到形参)
void f1(A aa)  // aa是形参,传值时会调用拷贝构造(无优化时)
{
    cout << "f1函数执行中:形参aa已创建" << endl;
}

// 场景2:传值返回函数(返回值是对象,无优化时会拷贝局部对象到返回临时对象)
A f2()
{
    A aa;  // 创建局部对象aa,调用构造函数(默认参数a=0)
    cout << "f2函数执行中:局部对象aa已创建" << endl;
    return aa;  // 返回局部对象,无优化时调用拷贝构造
}

int main()
{
    cout << "========== 测试1:构造临时对象直接初始化(优化场景)==========" << endl;
    // 原逻辑:1→构造临时A对象→拷贝构造aa1
    // 优化后:直接用1构造aa1,仅调用构造函数,省略临时对象和拷贝构造
    A aa1 = 1;
    cout << endl;

    cout << "========== 测试2:传值传参(无优化)==========" << endl;
    // aa1是已存在的对象,传值给f1的形参
    // 无优化:调用拷贝构造(aa1拷贝给f1的形参aa)
    // 执行完f1后,形参aa出作用域,调用析构
    f1(aa1);
    cout << endl;

    cout << "========== 测试3:隐式类型转换传参(优化场景)==========" << endl;
    // 原逻辑:1→构造临时A对象→拷贝构造f1的形参aa
    // 优化后:直接用1构造f1的形参aa,仅调用构造函数,省略临时对象和拷贝构造
    f1(1);
    cout << endl;

    cout << "========== 测试4:临时对象传参(优化场景)==========" << endl;
    // 原逻辑:A(2)构造临时对象→拷贝构造f1的形参aa
    // 优化后:直接构造f1的形参aa,仅调用构造函数,省略临时对象和拷贝构造
    f1(A(2));
    cout << endl;

    cout << "========== 测试5:传值返回(优化场景)==========" << endl;
    // f2返回局部对象,无优化:构造aa→拷贝构造返回临时对象→析构aa→析构临时对象
    // VS2019 Debug:优化为1次拷贝构造;VS2022 Debug/Release:直接构造,省略拷贝
    f2();
    cout << endl;

    cout << "========== 测试6:返回值直接初始化对象(深度优化)==========" << endl;
    // 原逻辑:f2返回临时对象→拷贝构造aa2→析构临时对象
    // VS2019 Debug:合并为1次拷贝构造;VS2022:直接构造aa2,省略所有拷贝
    A aa2 = f2();
    cout << endl;

    cout << "========== 测试7:返回值赋值给已有对象(无优化)==========" << endl;
    // 赋值重载无法优化:f2返回临时对象→调用赋值重载→析构临时对象
    // 核心:赋值是修改已有对象,而非创建新对象,编译器无法省略临时对象
    aa1 = f2();
    cout << endl;

    // 程序结束:局部对象aa1、aa2出作用域,依次析构
    cout << "========== 程序结束,析构局部对象 ==========" << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值