C++基础:(五)类和对象(下)—— static、友元和内部类

前言

除了基础的封装、继承和多态特性,C++还提供了static成员、友元机制和内部类等高级特性,以解决特定场景下的设计问题。static成员通过共享数据或方法实现类级别的操作,避免了全局变量的滥用;友元机制在严格封装的前提下,允许特定函数或类访问私有成员,提升了灵活性;内部类则通过嵌套关系实现逻辑上的紧密关联,优化了代码组织结构。这些特性不仅扩展了面向对象的设计维度,也为开发者提供了更高效的解决方案。本文将深入探讨其原理、应用场景及实践技巧,帮助读者掌握这些关键技术的核心思想。


一、再探构造函数

在 C++ 中,构造函数初始化成员变量有两种方式,除了在函数体内赋值外,还有初始化列表的方式:

  • 初始化列表以冒号 开头,后跟用逗号分隔 的成员变量列表,每个成员变量后通过括号指定初始值或表达式

  • 每个成员变量在初始化列表中只能出现一次,从语法角度看,初始化列表可视为成员变量定义和初始化的地方。

  • 对于引用类型成员变量、const 成员变量,以及没有默认构造函数的类类型变量,必须在初始化列表中进行初始化,否则会导致编译错误。

  • C++11 允许在成员变量声明时指定缺省值,这一缺省值主要供未在初始化列表中显式初始化的成员使用。

  • 建议优先使用初始化列表 初始化成员变量。因为即使某些成员未在初始化列表中显式列出,它们也会通过初始化列表进行初始化:若成员在声明时指定了缺省值,初始化列表会使用该缺省值;若未指定缺省值,内置类型成员是否初始化由编译器决定(C++ 标准未作规定);而自定义类型成员会调用其默认构造函数,若该类型没有默认构造函数,则会编译错误

  • 初始化列表中成员的初始化顺序 ,取决于它们在类中的声明顺序 ,与在初始化列表中的出现顺序无关。因此,建议成员的声明顺序与初始化列表中的顺序保持一致,以避免混淆。

对于初始化列表,我们可以做出如下总结:

  1. 无论是否显示写初始化列表,每个构造函数都有初始化列表;

  2. 无论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化。

我在这给出一些与初始化列表有关的代码示例:

cpp 复制代码
#include<iostream>
using namespace std;
class Time
{
public :
	Time(int hour)
		: _hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public :
	Date(int& x, int year = 1, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
		, _t(12)
		, _ref(x)
		, _n(1)
	{
		// error C2512: "Time": 没有合适的默认构造函数可⽤
		// error C2530 : "Date::_ref" : 必须初始化引⽤
		// error C2789 : "Date::_n" : 必须初始化常量限定类型的对象
	} 
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t; // 没有默认构造
	int& _ref; // 引⽤
	const int _n; // const
};
int main()
{
	int i = 0;
	Date d1(i);
	d1.Print();

	return 0;
}

了解了初始化列表的一些基本知识后,我们下面就来做一道题巩固一下:

下面程序的运行结果是什么( )

A. 输出 1 1

B. 输出 2 2

C. 编译报错

D. 输出1 随机值

E. 输出1 2

F. 输出2 1

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		: _a1(a)
		, _a2(_a1)
	{}

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};

int main()
{
	A aa(1);
	aa.Print();
}

分析:

这道题考查的是初始化列表的初始化顺序 。类中成员变量的初始化顺序由它们在类中的声明顺序决定,与初始化列表中的顺序无关。在类 A 中,_a2 先声明,_a1 后声明,所以会先初始化_a2,再初始化_a1。初始化时,_a2 使用的是尚未初始化的_a1 的值(此时_a1 是随机值),而_a1 被初始化为 1。成员变量声明时的缺省值 2 不会被使用,因为它们在初始化列表中被显式初始化了。因此程序输出 1 和一个随机值,答案选 D

二、类型转换

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()
{
	// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3
	// 编译器遇到连续构造+拷⻉构造->优化为直接构造
	A aa1 = 1;
	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 修饰的成员变量称为静态成员变量 ,其必须在类外进行初始化 ;静态成员变量为所有类对象所共享,不属于某个具体对象,不存储在对象中,而是位于静态区。

用 static 修饰的成员函数称为静态成员函数 ,这类函数没有 this 指针 ,因此只能访问其他静态成员,无法访问非静态成员;而非静态成员函数则可以访问任意静态成员变量和静态成员函数。

访问静态成员需突破类域,可通过**"类名::静态成员""对象. 静态成员"**的方式,且静态成员作为类的成员,同样受 public、protected、private 访问限定符的限制。

另外,静态成员变量不能在声明位置赋予缺省值初始化,因为缺省值用于构造函数初始化列表,而静态成员变量不属于某个对象,不经过构造函数初始化列表。

同样地,这里也给出一段代码作为示例:

cpp 复制代码
// 实现⼀个类,计算程序中创建出了多少个类对象
#include<iostream>
using namespace std;
class A
{
public :
	A()
	{
		++_scount;
	} 
	
	A
	(const A& t)
	{
		++_scount;
	} 
	
	~A()
	{
		--_scount;
	} 
	
	static int GetACount()
	{
		return _scount;
	}
private:
	// 类⾥⾯声明
	static int _scount;
};
// 类外⾯初始化
int A::_scount = 0;

int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;

	// 编译报错:error C2248: "A::_scount": ⽆法访问 private 成员(在"A"类中声明)
	//cout << A::_scount << endl;

	return 0;
}

四、友元

友元提供了一种突破类访问限定符封装的方式,分为友元函数友元类 ,只需在函数或类的声明前 加上 friend,并将该声明置于某个类的内部即可。

外部友元函数能够访问类的私有和保护成员 ,但它仅仅是一种声明,并非类的成员函数;友元函数可在类定义的任意位置声明,不受类访问限定符的限制,且一个函数可以作为多个类的友元函数

友元类中的所有成员函数都自动成为另一个类的友元函数,都能访问该类的私有和保护成员;不过友元类的关系是单向的,不具有交换性,比如 A 类是 B 类的友元,并不意味着 B 类是 A 类的友元;同时这种关系也不能传递,即若 A 是 B 的友元、B 是 C 的友元,A 也不会因此成为 C 的友元。

友元虽能带来一定便利,但会增加类之间的耦合度,破坏封装性,因此不宜过多使用。

下面同样给大家带来一段示例代码:

cpp 复制代码
#include<iostream>
using namespace std;

// 前置声明,都则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;
}

五、内部类

若一个类定义在另一个类的内部,则该内部类是独立的类,与全局定义的类相比,仅受外部类的类域和访问限定符限制 ,因此外部类的对象中不包含内部类。内部类默认是外部类的友元类

内部类本质上也是一种封装方式,当 A 类与 B 类关联紧密,且 A 类的实现主要供 B 类使用时,可将 A 类设计为 B 类的内部类;若将其置于 private 或 protected 位置,A 类就成为 B 类的专属内部类,其他地方无法使用。

参考代码如下:

cpp 复制代码
#include<iostream>
using namespace std;

class A
{ 
private:
    static int _k;
    int _h = 1;
public:
    class B // B默认就是A的友元
    { 
    public:
        void foo(const A& a)
        {
            cout << _k << endl;       //OK
            cout << a._h << endl;     //OK
        }
        
        int _b1;
    };
};

int A::_k = 1;

int main()
{
    cout << sizeof(A) << endl;

    A::B b;

    A aa;
    b.foo(aa);

    return 0;
}

六、匿名对象

通过**"类型(实参)"** 形式定义的对象称为匿名对象 ,而通过**"类型 对象名(实参)"** 形式定义的对象则是有名对象

匿名对象的生命周期仅局限于定义它的当前行,通常在需要临时定义一个对象并立即使用时,就可以采用匿名对象的形式。

下面是匿名对象的示例代码:

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++ 标准并未严格规定拷贝优化的具体方式,各编译器会根据实际情况自行处理。当前主流的较新版本编译器,能对单个表达式步骤中的连续拷贝进行合并优化;部分更新、更 "激进" 的编译器,甚至支持跨代码行、跨表达式的合并优化。

在 Linux 环境下,若要关闭构造相关的优化,可将目标代码复制到 test.cpp 文件中,然后使用g++ test.cpp -fno-elideconstructors的命令进行编译。

下面为大家提供编译器优化相关的图解和示例代码:

cpp 复制代码
#include<iostream>
using namespace std;

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;
    }
private:
        int _a1 = 1;
};

void f1(A aa)
{}

A f2()
{
    A aa;
    return aa;
} 

int main()
{
    // 传值传参
    // 构造+拷⻉构造
    A aa1;
    f1(aa1);
    cout << endl;

    // 隐式类型,连续构造+拷⻉构造->优化为直接构造
    f1(1);

    // ⼀个表达式中,连续构造+拷⻉构造->优化为⼀个构造
    f1(A(2));
    cout << endl;

    cout << "***********************************************" << endl;
    // 传值返回
    // 不优化的情况下传值返回,编译器会⽣成⼀个拷⻉返回对象的临时对象作为函数调⽤表达式的返回值

    // ⽆优化 (vs2019 debug)
    // ⼀些编译器会优化得更厉害,将构造的局部对象和拷⻉构造的临时对象优化为直接构造(vs2022debug)
    f2();
    cout << endl;

    // 返回时⼀个表达式中,连续拷⻉构造+拷⻉构造->优化⼀个拷⻉构造 (vs2019 debug)
    // ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,将构造的局部对象aa和拷⻉的临时对象和接收返回值对象aa2优化为⼀个直接构造。(vs2022 debug)
    A aa2 = f2();
    cout << endl;

    // ⼀个表达式中,开始构造,中间拷⻉构造+赋值重载->⽆法优化(vs2019 debug)
    // ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,将构造的局部对象aa和拷⻉临时对象合并为⼀个直接构造(vs2022 debug)
    aa1 = f2();
    cout << endl;

    return 0;
}

总结

本期博客我为大家介绍了C++类和对象中一些收尾的知识,包括static成员、友元函数和友元类、内部类、匿名对象等。这些知识在C++中也是基础且重要的。希望大家能多多支持我的博客!咱们下期见!

相关推荐
yongui478342 小时前
基于MATLAB的8QAM调制解调仿真与BER性能分析
开发语言·matlab
早日退休!!!3 小时前
C 内存布局
c语言·开发语言
linuxoffer3 小时前
composer 安装与开启PHP扩展支持
开发语言·php·composer
come112343 小时前
Go 和云原生 的现状和发展前景
开发语言·云原生·golang
专家大圣3 小时前
Bililive-go+cpolar:跨平台直播录制的远程管理方案
开发语言·网络·后端·golang·内网穿透·设计工具
Eiceblue3 小时前
Python 将 HTML 转换为纯文本 TXT (HTML 文本提取)
开发语言·vscode·python·html
张人玉4 小时前
C# TCP - 串口转发
开发语言·tcp/ip·c#
苦逼大学生被编程薄纱4 小时前
C++ 容器学习系列|vector 核心知识全解析,铺垫下一期模拟实现
开发语言·c++·学习
ajassi20004 小时前
开源 C# 快速开发(四)自定义控件--波形图
开发语言·开源·c#