【C++】static、友元、内部类、匿名对象与拷贝编译器优化全解析

📅 发布时间:2026/10/12 2:22:01
【C++】static、友元、内部类、匿名对象与拷贝编译器优化全解析
目录一.static和const1.const成员的初始化2.static成员的初始化二.友元三.内部类四.匿名对象五.对象拷贝时的编译器优化一.static和const1.const成员的初始化类中的 const 成员变量是隶属于单个对象的常量每一个对象都可以拥有一份独立的 const 成员不同对象的 const 成员值可以互不相同。const 成员一旦初始化完成在对象的整个生命周期中都不能被修改。由于常量不能先创建再赋值因此不能在类内直接对 const 成员变量赋值也不能在构造函数的函数体内赋值必须放在构造函数的初始化列表中完成初始化。初始化列表是对象成员创建的阶段此时才可以给 const 成员赋初始值进入构造函数体之后成员已经创建完毕就无法再对 const 成员赋值。类外普通的 const 局部 / 全局变量要求定义的同时直接初始化定义之后就不能再修改。#include iostream using namespace std; class TestConst { public: // const成员必须在初始化列表初始化 TestConst(int val) : _data(val) { // _data val; // 错误构造函数体内不能给const成员赋值 } private: const int _data; // const成员变量属于对象类内仅声明 }; int main() { TestConst t1(100); TestConst t2(200); // t1和t2的_data各自独立 const int a 10; // 类外普通const变量定义即初始化 return 0; }2.static成员的初始化static 修饰的类成员变量属于整个类不属于任何一个对象所有类实例共享同一份 static 成员它存储在静态存储区不计入对象内存大小。类内部书写的 static 成员仅仅是声明并不会分配内存必须在类外全局作用域进行定义和初始化不能使用构造函数的初始化列表。初始化只在整个程序运行期间执行一次早于任何对象的创建。用 static 修饰的成员变量叫做静态成员变量静态成员变量必须在类外完成初始化。静态成员变量被该类的所有对象共享不属于某一个具体对象不会存放在对象内存空间里而是存储在静态区。用 static 修饰的成员函数称为静态成员函数静态成员函数没有 this 指针。静态成员函数内部可以访问其他静态成员但是不能访问非静态成员原因就是缺少 this 指针无法找到属于对象的非静态成员。而非静态成员函数没有这个限制可以访问任意静态成员变量与静态成员函数。访问静态成员有两种方式使用类名加两个冒号访问静态成员或者使用对象点静态成员两种写法都可以突破类域访问。静态成员依然属于类的成员会受到 public、protected、private 访问限定符的权限约束。静态成员变量不能在类内声明的位置设置缺省值初始化缺省初始化是构造函数初始化列表的机制而静态成员不属于任何对象不走构造函数初始化列表。static const 是类级别的静态常量结合了 static 的共享特性与 const 只读特性。C17 标准之前需要在类外单独定义初始化C17 及以后可以使用 inline 在类内直接初始化。static 成员不受对象生命周期影响程序启动时分配内存程序结束才释放。#include iostream using namespace std; class TestStatic { private: static int _s_data; // static成员类内仅声明不分配内存 static const int _sc_data; // static const 静态常量类内声明 }; // static成员类外定义初始化前面不要写static关键字 int TestStatic::_s_data 50; // C17之前 static const 类外初始化 const int TestStatic::_sc_data 200; int main() { cout TestStatic::_s_data endl; cout TestStatic::_sc_data endl; return 0; }二.友元友元是 C 提供的一种权限机制可以让外部函数或者另一个类访问本类内部被保护的私有成员。友元分为友元函数和友元类。友元不是类的成员不受类里面 public、private、protected 访问限定符的限制。友元关系是单向的不能传递也不能继承。友元的作用场景主要是当外部函数需要直接访问类的私有成员又不想增加过多的公有接口时使用最典型的例子就是运算符重载比如流插入运算符重载。使用友元需要注意1.友元声明写在类内部声明位置不受访问限定符影响友元函数不是类成员没有 this 指针2.友元关系单向A 把 B 设为友元不代表 B 自动把 A 当成友元3.友元不能传递A 的友元是 BB 的友元并不自动是 C4.友元不能继承5.友元会破坏面向对象的封装特性所以不能滥用只在必要场景使用。#includeiostream using namespace std; // 类B的前置声明 // 作用告诉编译器B是一个类这样类A中就可以使用B的引用类型 // 前置声明仅识别类名不能查看类内部成员不能实例化对象 class B; class A { // 友元声明声明全局函数func是类A的友元 // 友元声明位置不受访问限定符限制写在私有区公有区都生效 friend void func(const A aa, const B bb); private: int _a1 1; int _a2 2; }; class B { // 同一个全局函数func同时声明为类B的友元 // 因此func同时拥有访问A、B私有成员的权限 friend void func(const A aa, const B bb); private: int _b1 3; int _b2 4; }; // 全局友元函数定义不属于A也不属于B是独立全局函数 void func(const A aa, const B bb) { // 友元函数可以直接访问A的私有成员 cout aa._a1 endl; // 友元函数可以直接访问B的私有成员 cout bb._b1 endl; } int main() { A aa; B bb; func(aa, bb); //调用友元函数 return 0; }三.内部类内部类就是在一个类的内部再定义另外一个类外面的类叫做外部类里面嵌套的类就是内部类。内部类是独立的类属于外部类的作用域但是不属于外部类的对象。就好比刘备是曹操的客卿不是他帐下的文官武将离开不算是背叛。内部类可以直接访问外部类的所有成员包含外部类的私有成员外部类想要访问内部类的成员需要借助内部类的对象。内部类本身受访问限定符控制外部类可以把内部类设置为公有的、保护的或者私有的。内部类是外部类的友元。使用场景当一个类仅仅只给外部类使用不会单独在外部大量使用的时候就适合定义为内部类用来封装关联度很高的类型让代码结构更清晰。比如链表类里面定义节点类节点只供链表使用就可以把节点定义成内部类。使用注意事项内部类只是放在外部类的作用域里面外部类对象和内部类对象互相独立内部类对象不会包含在外部类对象里面不会增大外部类对象的大小。在外部类外面去实例化内部类需要使用外部类名字加两个冒号再加内部类名字。内部类可以访问外部类成员但是外部类访问内部类成员必须创建内部类对象。内部类可以定义成员变量、成员函数、静态成员。#includeiostream using namespace std; class A { private: static int _k; // A的静态成员变量存放在静态区不计入A对象大小 int _h 1; // A的非静态成员变量属于A的对象 public: // 内部类B定义在类A里面属于A的作用域 // 内部类B默认就是外部类A的友元B可以访问A的所有成员包括私有 class B { public: void foo(const A a) { cout _k endl; // OK内部类可以访问外部类A的静态私有成员 cout a._h endl; // OK内部类可以访问外部类A的非静态私有成员 } int _b1; // 内部类B自己的成员变量属于B对象不属于A对象 }; }; // 外部类A的静态成员变量类外定义并初始化 int A::_k 1; int main() { // 计算A对象的大小A中只有非静态成员_hint占4字节输出4 cout sizeof(A) endl; // 在类外实例化内部类B写法外部类名字加两个冒号再加内部类名字 A::B b; A aa; b.foo(aa); //调用内部类对象的成员函数 return 0; } //内部类 B 定义在 A 里面内部类默认是外部类的友元所以 B 的成员函数可以直接访问 A 的私有成员。 //内部类 B 只是属于 A 的作用域B 的成员变量_b1 不属于 A不计入外部类 A 的对象大小。 //sizeof(A)只统计 A 的非静态成员变量A 里面只有_h所以结果是 4。静态成员_k放在静态区不计入对象大小。 //类外创建内部类对象需要使用外部类名字加两个冒号再加内部类名字。 //区分B 是 A 的内部类友元但是 A不能直接访问 B 的私有成员如果 A 要访问 B 成员需要创建 B 的对象。四.匿名对象匿名对象也叫临时对象是没有名字、没有绑定变量名的对象。创建匿名对象时不定义对象名直接调用构造函数生成匿名对象的生命周期很短所在的这一行语句执行结束匿名对象就会立刻被销毁。匿名对象可以直接调用成员函数也可以作为函数的实参传递或者作为函数返回值。匿名对象属于右值可以被 const 引用接收普通引用不能接收匿名对象。使用场景当只需要临时使用一次对象不需要后续反复使用这个对象时就可以使用匿名对象简化代码书写。比如调用函数传参、一次性调用成员函数、函数返回临时对象。使用注意事项匿名对象生命周期只在当前语句语句执行完马上销毁普通的引用不能接收匿名对象但是 const 引用可以延长匿名对象的生命周期用引用接收匿名对象时必须是 const 引用不要尝试用普通引用接收匿名对象会编译报错匿名对象没有名字后续无法再访问如果用 const 引用接住匿名对象匿名对象生命周期会延长到这个引用的生命周期结束。#includeiostream using namespace std; 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) { // 此处省略逻辑直接返回n return n; } }; int main() { A aa1; // 定义有名对象调用构造函数main结束才析构 // A aa1(); // 不能这么写编译器会把它识别成函数声明不是对象定义 // 创建匿名对象没有变量名本行执行结束立刻析构 A(); A(1); A aa2(2); // 有名对象main结束才析构 // Solution() 创建匿名对象直接调用成员函数用完马上销毁 Solution().Sum_Solution(10); return 0; }五.对象拷贝时的编译器优化对象拷贝时的编译器优化也叫返回值优化RVO/NRVO是 C 编译器做的一种优化手段。当函数返回一个局部对象或者用临时对象去初始化另一个对象时编译器会省略拷贝构造直接在目标对象的内存空间里构造对象省去一次对象拷贝和析构。RVO 指返回匿名临时对象的优化NRVO 是命名返回值优化指返回函数内部定义的有名局部对象。这个优化不是语法强制要求属于编译器可选优化C17 之后变成强制优化。特点优化生效时不会调用拷贝构造函数直接在外部目标内存构造对象减少内存拷贝提升运行效率。优化的前提是返回的是函数内的局部对象不能是外部对象或者引用。使用场景函数返回自定义类对象并用返回值直接初始化外部对象。比如函数内创建对象 return 返回main 里面直接使用A a func();接收这时编译器会触发返回值优化。注意事项RVO、NRVO 是编译器优化不是语言语法C17 标准前不同编译器开启优化选项才会生效关闭优化后会执行拷贝构造不能在拷贝构造函数里写业务逻辑不要依赖拷贝构造一定会执行返回的对象必须是函数内部局部对象返回全局对象、静态对象、引用都不能触发该优化NRVO 是优化有名局部对象RVO 优化匿名临时对象。#includeiostream 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; }; // 值传参形参是A类型对象传参时会调用拷贝构造 void f1(A aa) { } // 返回值优化示例函数返回函数内有名局部对象NRVO A f2() { A aa; return aa; } int main() { // 1.传值传参 A aa1; f1(aa1); cout endl; // 隐式类型转换 编译器优化连续构造拷贝构造优化为直接构造 f1(1); // 一个表达式内连续构造拷贝构造优化为一次直接构造 f1(A(2)); cout endl; cout **************************************** endl; // 2.传值返回NRVO返回值优化 // 不优化的场景f2内部构造aareturn时拷贝生成临时对象 f2(); cout endl; // 用返回值直接初始化对象优化连续两次拷贝构造合并优化 A aa2 f2(); cout endl; // 赋值场景先生成临时对象再调用赋值重载。 // 构造拷贝构造赋值重载无法合并成直接构造 aa1 f2(); cout endl; return 0; }当函数形参采用值传递时对象会发生拷贝。f1 (A aa) 中传入已有对象 aa1会调用拷贝构造函数完成形参对象的创建。当传入整数字面量 1 时会触发隐式类型转换原本流程是先用 1 构建匿名临时对象再将该对象拷贝至形参经过编译器优化后会直接在形参的内存空间完成对象构造省去拷贝环节。f1 (A (2)) 的原理与之类似A (2) 会生成匿名对象未经优化时需要拷贝给形参编译器优化后直接在形参内存完成对象构造消除拷贝开销。函数 A f2 () 返回函数内部定义的有名局部对象 aa属于 NRVO 有名返回值优化。直接调用 f2 () 时函数内部先构造局部对象 aareturn 阶段原本需要拷贝生成临时匿名对象编译器优化后直接在临时对象的内存上构造 aa减少一次拷贝。A aa2 f2 () 属于对象初始化场景支持 RVO 与 NRVO 优化编译器可以将函数内部的局部对象直接构造到外部对象 aa2 的内存空间整个过程不会触发拷贝构造函数。而 aa1 f2 () 属于赋值场景无法合并为直接构造。函数调用会先产生返回值对应的临时对象再调用赋值重载函数将临时对象的数据赋值给已经存在的对象 aa1。