目录
一.static和const
1.const成员的初始化
类中的 const 成员变量是隶属于单个对象 的常量,每一个对象都可以拥有一份独立的 const 成员,不同对象的 const 成员值可以互不相同。const 成员一旦初始化完成,在对象的整个生命周期中都不能被修改。由于常量不能先创建再赋值,因此不能在类内直接对 const 成员变量赋值,也不能在构造函数的函数体内赋值 ,必须放在构造函数的初始化列表 中完成初始化。初始化列表是对象成员创建的阶段,此时才可以给 const 成员赋初始值;进入构造函数体之后,成员已经创建完毕,就无法再对 const 成员赋值。
类外普通的 const 局部 / 全局变量,要求定义的同时直接初始化,定义之后就不能再修改。
cpp
#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 只读特性。C++17 标准之前,需要在类外单独定义初始化;C++17 及以后可以使用 inline 在类内直接初始化。static 成员不受对象生命周期影响,程序启动时分配内存,程序结束才释放。
cpp
#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;
// C++17之前 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 的友元是 B,B 的友元并不自动是 C;
4.友元不能继承;
5.友元会破坏面向对象的封装特性,所以不能滥用,只在必要场景使用。
cpp
#include<iostream>
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;
}
三.内部类
内部类,就是在一个类的内部再定义另外一个类,外面的类叫做外部类,里面嵌套的类就是内部类。内部类是独立的类,属于外部类的作用域,但是不属于外部类的对象。就好比刘备是曹操的客卿,不是他帐下的文官武将,离开不算是背叛。内部类可以直接访问外部类的所有成员,包含外部类的私有成员;外部类想要访问内部类的成员,需要借助内部类的对象。内部类本身受访问限定符控制,外部类可以把内部类设置为公有的、保护的或者私有的。内部类是外部类的友元。
使用场景:当一个类仅仅只给外部类使用,不会单独在外部大量使用的时候,就适合定义为内部类,用来封装关联度很高的类型,让代码结构更清晰。比如链表类里面定义节点类,节点只供链表使用,就可以把节点定义成内部类。
使用注意事项:内部类只是放在外部类的作用域里面,外部类对象和内部类对象互相独立,内部类对象不会包含在外部类对象里面,不会增大外部类对象的大小。在外部类外面去实例化内部类,需要使用外部类名字加两个冒号再加内部类名字。内部类可以访问外部类成员,但是外部类访问内部类成员,必须创建内部类对象。内部类可以定义成员变量、成员函数、静态成员。
cpp
#include<iostream>
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中只有非静态成员_h,int占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 引用接住匿名对象,匿名对象生命周期会延长到这个引用的生命周期结束。
cpp
#include<iostream>
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 是命名返回值优化,指返回函数内部定义的有名局部对象。这个优化不是语法强制要求,属于编译器可选优化,C++17 之后变成强制优化。
特点:优化生效时,不会调用拷贝构造函数,直接在外部目标内存构造对象,减少内存拷贝,提升运行效率。优化的前提是返回的是函数内的局部对象,不能是外部对象或者引用。
使用场景:函数返回自定义类对象,并用返回值直接初始化外部对象。比如函数内创建对象 return 返回,main 里面直接使用A a = func();接收,这时编译器会触发返回值优化。
注意事项:RVO、NRVO 是编译器优化,不是语言语法;C++17 标准前,不同编译器开启优化选项才会生效,关闭优化后会执行拷贝构造;不能在拷贝构造函数里写业务逻辑,不要依赖拷贝构造一定会执行;返回的对象必须是函数内部局部对象,返回全局对象、静态对象、引用都不能触发该优化;NRVO 是优化有名局部对象,RVO 优化匿名临时对象。
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;
};
// 值传参:形参是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内部构造aa,return时拷贝生成临时对象
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 () 时,函数内部先构造局部对象 aa,return 阶段原本需要拷贝生成临时匿名对象;编译器优化后,直接在临时对象的内存上构造 aa,减少一次拷贝。A aa2 = f2 () 属于对象初始化场景,支持 RVO 与 NRVO 优化,编译器可以将函数内部的局部对象直接构造到外部对象 aa2 的内存空间,整个过程不会触发拷贝构造函数。而 aa1 = f2 () 属于赋值场景,无法合并为直接构造。函数调用会先产生返回值对应的临时对象,再调用赋值重载函数,将临时对象的数据赋值给已经存在的对象 aa1。
