C++ 类与对象(下)—— 进阶特性与底层机制解析(构造函数初始化,类型转换,static成员,友元,内部类,匿名对象)
一、构造函数初始化列表:给成员变量 “精准出生证明”
在 C++ 中,构造函数对成员变量的初始化方式有 初始化列表 和 函数体内赋值 两种。初始化列表是构造函数的一个重要特性,它允许在对象创建时对成员变量进行初始化。与在构造函数体内赋值不同,初始化列表是在成员变量定义时就进行初始化,这在效率和处理特定类型成员(如 const
成员、引用成员)上有显著优势。
二、基本语法
初始化列表位于构造函数的参数列表之后,函数体之前,以冒号" :
" 开始,成员变量的初始化用逗号" ,
" 分隔。其基本形式如下:
Date(参数列表)
: 成员变量1(初始化值1),
成员变量2(初始化值2),
......... {
// 构造函数体
}
这里的 Date
是类的名称,参数列表
是构造函数接收的参数,成员变量1
、成员变量2
等是类中定义的成员变量,初始化值1
、初始化值2
等是用于初始化成员变量的值。
补充: 构造函数的执行顺序为:先调用该类的初始化列表,最后执行该类的构造函数体。
有初始化列表情况下:用基本数据类型(如 int
、double
、指针等)来举例,自定义类型也是同理,如下图:
无初始化列表情况下:自定义类型(但有另一个类的对象的默认构造函数)如下图:
在 C++ 中,当类的构造函数 没有显式使用初始化列表 时,成员变量会经历 默认初始化(Default Initialization):
-
基本数据类型(如
int
、double
、指针等):
默认初始化不会赋予任何初始值,它们的值是未定义的(Undefined),可能是内存中的随机值(俗称 “垃圾值”)。 -
自定义类型(如另一个类的对象):
会调用其 默认构造函数 进行初始化(即使该自定义类型没有写初始化列表)。
- 执行顺序:
- 先通过 “空的初始化列表” 对成员进行默认初始化(基本类型未定义,自定义类型调用默认构造函数)。
- 再进入构造函数体,对成员进行赋值(覆盖默认初始化后的值)。
如果没有另一个类的对象和无初始化列表基本数据类型一样会执行空的初始化列表,感觉像跳过一样,其实没有跳过然后再执行构造函数体,如下图:
三、使用细节
(一)初始化顺序
成员变量的初始化顺序由它们在类中声明的顺序决定,而不是在初始化列表中出现的顺序。例如:
在这个例子中,尽管在初始化列表里 b
先出现,但由于 a
在类中先声明,所以 a
会先初始化。此时 b
未初始化,a
会得到未定义的值。因此,在编写初始化列表时,要确保初始化顺序符合逻辑,避免出现未定义行为。
(二)必须使用初始化列表的情况
- 初始化
const
成员:const
成员在定义后不能被赋值,只能在初始化时指定值,所以必须使用初始化列表进行初始化。 - 初始化引用成员:引用在定义时必须绑定到一个对象,之后不能再绑定到其他对象,因此也需要在初始化列表中完成初始化。
- 初始化没有默认构造函数的自定义类型成员:如果类的成员是另一个自定义类型,且该自定义类型没有默认构造函数(无参构造函数或全缺省构造函数),则必须在初始化列表中显式调用其带参构造函数。
(三)效率优势
对于自定义类型的成员变量,使用初始化列表可以直接调用其合适的构造函数进行初始化,避免了先调用默认构造函数再进行赋值的额外开销。
四、示例代码
(一)基本使用示例
#include <iostream>
using namespace std;class Point {
private:int x;int y;
public:// 使用初始化列表的构造函数Point(int a, int b) : x(a), y(b) {cout << "Point 对象已创建,坐标为 (" << x << ", " << y << ")" <<endl;}
};int main() {Point p(3, 4);return 0;
}
在这个例子中,Point
类的构造函数使用初始化列表将成员变量 x
和 y
分别初始化为传入的参数 a
和 b
。
(二)初始化 const
成员示例
此例中,MyClass
类有一个 const
成员 value
,必须在初始化列表中进行初始化。
(三)初始化引用成员示例
在这个示例中,RefExample
类的成员 ref
是一个引用,通过初始化列表绑定到传入的参数 num
。
(四)初始化没有默认构造函数的自定义类型成员示例
这里 MyClass
类包含一个 SubClass
类型的成员 sub
,SubClass
没有默认构造函数,因此必须在 MyClass
的初始化列表中显式调用 SubClass
的带参构造函数。
补充:既然初始化列表这么好又不用执行别的,反而函数体内赋值每次都需要先执行初始化列表,不如直接用初始化列表,还可以省略步骤,岂不是更快?
初始化列表确实有很多优点,用它来初始化成员变量既方便又可能让程序运行得更快,但也不能一股脑全用初始化列表,不用函数体里赋值。下面给你仔细说说为啥。
初始化列表的好处
- 速度快:初始化列表就像是给成员变量直接 “设定出生状态”。而在构造函数里赋值呢,成员变量得先有个默认的 “出生状态”,然后再改成你想要的状态。这就好比你直接去市场买一只你想要的小狗,和先随便抱一只小狗回家,再把它换成你想要的那只,肯定是直接买更省事、更快。
- 有些情况必须用:有些成员变量就像有 “特殊脾气”,比如引用类型和
const
类型的成员变量,必须得用初始化列表来初始化。这就像有些游戏规则规定,必须先做完特定任务才能开始玩游戏一样。
构造函数里赋值也有它的用武之地
- 复杂情况更方便:要是成员变量的初始化得经过一番复杂的计算或者判断循环等等,又或者得依赖其他成员变量,在构造函数里赋值就更合适。比如说,你要根据两个成员变量算出它们的和,再把这个和赋值给另一个成员变量,在构造函数里写这些计算步骤,就会更清楚明白。
- 代码好懂又好改:如果初始化的过程特别复杂,把代码写在构造函数里,别人看代码的时候就能更容易理解你在做什么。就像写一篇文章,把复杂的内容分成一段一段写清楚,别人读起来才不会一头雾水。
总结
初始化列表和在构造函数里赋值各有各的好。要是成员变量能直接设定初始值,或者是引用类型、const
类型的变量,用初始化列表比较好;要是初始化过程复杂,或者为了让代码更清楚,就在构造函数里赋值。得根据实际情况来选择用哪种方式。
补充:
五,在 C++11 之前,我们只能在构造函数的初始化列表或者函数体中对类的成员变量进行初始化。而 C++11 引入了一个新特性,允许我们在成员变量声明的地方直接给它们设置缺省值。下面我们结合例子详细解释相关规则。
1. 缺省值在未显式初始化时的使用
(->引用类型和const类型的成员变量自定义类型成员是否可以用缺省值?)
当成员变量在声明处设置了缺省值,且在构造函数的初始化列表中没有显式对其进行初始化,那么初始化列表会使用这个缺省值来初始化该成员。
在上述代码中,num
成员变量在声明时被赋予了缺省值 10
。在构造函数里没有对 num
进行显式初始化,因此 num
会使用这个缺省值 10
完成初始化。
2. 未设置缺省值的内置类型成员
对于没有在声明处设置缺省值,并且在初始化列表中也没有显式初始化的内置类型成员,其是否初始化取决于编译器,C++ 标准并未对此作出规定。
在这个例子里,num
是一个内置类型(int
),它既没有缺省值,在构造函数初始化列表中也未被显式初始化,所以 num
的值是不确定的,不同编译器可能有不同表现。
3. 未显式初始化的自定义类型成员
如果成员是自定义类型,并且在初始化列表中没有显式初始化,那么会调用该自定义类型的默认构造函数。若该自定义类型没有默认构造函数,就会导致编译错误。
#include <iostream>// 自定义类型,有默认构造函数
class SubClass {
public:SubClass() {std::cout << "SubClass 默认构造函数被调用" << std::endl;}
};// 自定义类型,没有默认构造函数
class AnotherSubClass {
public:AnotherSubClass(int value) {std::cout << "AnotherSubClass 带参数构造函数被调用" << std::endl;}
};class MyClass {
public:SubClass sub; // 有默认构造函数的自定义类型成员AnotherSubClass anotherSub; // 没有默认构造函数的自定义类型成员// 构造函数,未显式初始化自定义类型成员MyClass() {}
};int main() {MyClass obj;return 0;
}
在上述代码中,SubClass
有默认构造函数,所以 MyClass
的构造函数在未显式初始化 sub
时,会自动调用 SubClass
的默认构造函数。而 AnotherSubClass
没有默认构造函数,当 MyClass
的构造函数未显式初始化 anotherSub
时,就会导致编译错误。
建议
尽量使用初始化列表来初始化成员变量,这样能让代码更加清晰,并且可以避免一些因未初始化而导致的潜在问题。如果某个成员变量有合适的缺省值,在声明处设置缺省值可以作为一种安全的兜底方案。
二、类型转换:让对象 “灵活变身”
补充:被优化掉的拷贝构造函数
理论上的步骤
在早期的 C++ 标准中,像 A d1 = 42;
这样的语句理论上会经历以下步骤:
- 首先,使用
42
调用A
类的构造函数A(int value)
创建一个临时的A
类型对象。 - 然后,使用这个临时对象调用拷贝构造函数来初始化
d1
。
现代编译器的优化
然而,现代的 C++ 编译器通常会进行 “拷贝省略(Copy Elision)” 优化。对于 A d1 = 42;
,编译器会直接使用 42
调用 A
类的构造函数来初始化 d1
,而不会创建临时对象,也不会调用拷贝构造函数。这是为了提高程序的性能,避免不必要的对象创建和复制操作
但是我们可以调整代码以避免编译器优化
为了尽可能地阻止编译器进行优化,我们可以对代码进行修改,把 A d1 = 42;
拆分成更明确的步骤,并且添加赋值运算符重载函数,从而更好地观察对象的创建和赋值过程。以下是修改后的代码:
代码解释
A
类:A()
:默认构造函数,用于创建A
类的对象。A(int value)
:带参数的构造函数,使用传入的int
值初始化A_value
。A(const A& other)
:拷贝构造函数,用于创建一个新对象,该对象是另一个A
类对象的副本。A& operator=(const A& other)
:赋值运算符重载函数,用于将一个A
类对象的值赋给另一个A
类对象。
main
函数:A d1;
:调用默认构造函数创建d1
对象。A temp = A(42);
:显式调用带参数的构造函数创建临时对象temp
。d1 = temp;
:调用赋值运算符重载函数,将temp
的值赋给d1
。
1. 内置类型到类类型的隐式转换
当我们需要将一个内置类型(如int
、double
等)隐式转换为某个类类型的对象时,只需要在类中定义一个接受该内置类型参数的构造函数即可。
示例代码:
class MyInt {
public:MyInt(int value): m_value(value) {} // 允许int到MyInt的隐式转换void print() const {std::cout << m_value << std::endl; }private:int m_value;
};void printNumber(const MyInt& num) {num.print();
}int main() {printNumber(42); // 隐式调用MyInt的构造函数:MyInt(42)return 0;
}
在上述代码中,printNumber
函数需要一个MyInt
类型的参数,但我们直接传递了一个int
类型的值42
。编译器会自动调用MyInt(int)
构造函数生成一个临时对象,完成隐式类型转换。
2. 禁止隐式转换:explicit
关键字
隐式转换虽然方便,但有时会导致意外的行为。例如,如果构造函数有多个参数,或者某些场景下不希望自动转换,可以使用explicit
关键字标记构造函数,强制要求显式调用。
示例代码:
class ExplicitInt {
public:explicit ExplicitInt(int value): m_value(value) {} // 必须显式调用void print() const {std::cout << m_value << std::endl; }private:int m_value;
};void printExplicit(const ExplicitInt& num) {num.print();
}int main() {// printExplicit(42); // 错误:不能隐式转换printExplicit(ExplicitInt(42)); // 正确:显式构造对象return 0;
}
通过添加explicit
,ExplicitInt
的构造函数不再支持隐式转换,必须在代码中明确创建对象。
3. 类类型之间的隐式转换
类类型的对象之间也可以进行隐式转换,这需要满足以下条件之一:
-
转换构造函数:目标类定义了接受源类对象为参数的构造函数。
-
类型转换运算符:源类定义了转换为目标类型的运算符。
3.1 转换构造函数
3.1.1 定义与原理
转换构造函数指的是目标类定义的接受源类对象作为参数的构造函数。当需要目标类对象,而提供的却是源类对象时,编译器会自动调用这个构造函数,把源类对象转换为目标类对象。
3.1.2 示例代码
#include <iostream>// 源类
class Meter {
public:Meter(double value): m_value(value) {}double getValue() const {return m_value; }
private:double m_value;
};// 目标类
class Centimeter {
public:// 转换构造函数Centimeter(const Meter& meter) : m_value(meter.getValue() * 100) {}void display() const {std::cout << "Centimeter: " << m_value << std::endl;}
private:double m_value;
};// 接受 Centimeter 对象的函数
void printCentimeter(const Centimeter& cm) {cm.display();
}int main() {Meter meter(2.5);// 隐式转换:Meter 对象转换为 Centimeter 对象printCentimeter(meter);return 0;
}
3.1.3代码解释
Meter
类是源类,Centimeter
类是目标类。Centimeter
类的构造函数Centimeter(const Meter& meter)
属于转换构造函数,它能把Meter
对象转换为Centimeter
对象。- 在
main
函数中,调用printCentimeter(meter)
时,由于printCentimeter
函数需要Centimeter
对象,而传入的是Meter
对象,编译器会自动调用Centimeter
的转换构造函数,将Meter
对象转换为Centimeter
对象。
3.2 类型转换运算符
3.2.1 定义与原理
类型转换运算符是源类定义的转换为目标类型的运算符。当需要目标类型对象,而提供的是源类对象时,编译器会自动调用这个运算符,把源类对象转换为目标类型对象。
3.2.2 示例代码
#include <iostream>// 源类
class Pound {
public:Pound(double value): m_value(value) {}// 类型转换运算符operator double() const {return m_value;}
private:double m_value;
};// 接受 double 类型参数的函数
void printWeight(double weight) {std::cout << "Weight in kg: " << weight * 0.453592 << std::endl;
}int main() {Pound pound(10);// 隐式转换:Pound 对象转换为 double 类型printWeight(pound);return 0;
}
3.2.3 代码解释
Pound
类是源类,double
是目标类型。Pound
类的operator double()
是类型转换运算符,它能把Pound
对象转换为double
类型。- 在
main
函数中,调用printWeight(pound)
时,由于printWeight
函数需要double
类型参数,而传入的是Pound
对象,编译器会自动调用Pound
的类型转换运算符,将Pound
对象转换为double
类型。
3. 3注意事项
3. 3.1 二义性问题
如果同时定义了转换构造函数和类型转换运算符,可能会引发二义性问题。例如:
#include <iostream>class A;class B {
public:B() {}B(const A& a);
};class A {
public:A() {}operator B() const;
};B::B(const A& a) {std::cout << "Conversion constructor called" << std::endl;
}A::operator B() const {std::cout << "Type conversion operator called" << std::endl;return B();
}void func(const B& b) {}int main() {A a;// 二义性问题,编译器不知道该使用转换构造函数还是类型转换运算符func(a); return 0;
}
在这个例子中,调用 func(a)
时,编译器既可以调用 B
的转换构造函数,也可以调用 A
的类型转换运算符,从而导致编译错误。
4. 隐式转换的风险与最佳实践
虽然隐式转换提供了便利,但也可能引发问题:
-
意外的行为:编译器可能在开发者未察觉的情况下进行转换,导致逻辑错误。
-
性能损耗:生成临时对象可能带来额外开销。
建议:
-
对单参数构造函数使用
explicit
,除非明确需要隐式转换。 -
谨慎使用类型转换运算符,或将其标记为
explicit
(C++11起支持)。
C++11的显式类型转换运算符示例:
class SafeInt {
public:explicit operator int() const {return m_value; } // 必须显式转换
private:int m_value;
};int main() {SafeInt si;// int i = si; // 错误:不能隐式转换int j = static_cast<int>(si); // 正确:显式转换return 0;
}
总结
C++中的类型转换机制赋予开发者极大的灵活性,但也需要谨慎使用:
-
内置类型到类类型的转换依赖构造函数。
-
explicit
关键字用于禁止隐式转换。 -
类类型之间可通过转换构造函数或类型转换运算符实现隐式转换。
-
优先使用显式转换以提高代码安全性。
三、static 成员:类的 “全局管家”
在 C++ 中,静态成员(Static Members) 是属于类本身的成员,而非类的某个对象。它们不依赖于类的实例存在,可以通过类名直接访问。静态成员分为 静态成员变量 和 静态成员函数,是实现类级别的数据共享和工具方法的重要机制。
1. 静态成员变量(Static Member Variables)
基本概念
- 声明与定义:
- 在类内用
static
关键字声明,需在类外进行定义(分配内存),否则链接时会报错。
class MyClass { public:private://成员变量 -- 属于每一个类对象,存储在对象里面int _a1 = 1;int _a2 = 2;// 静态成员变量 -- 属于类,属于类的每个对象共享,存储到静态区static int s_count; };int MyClass::s_count = 0; // 全局位置,类外定义并初始化(必须)
- 在类内用
- 存储特性:
- 静态成员变量存储在全局数据区(静态存储区),不属于任何对象,所有对象共享同一份实例。
- 无论创建多少个类对象,静态成员变量只有一份拷贝。
访问方式
- 通过类名:
类名::静态成员变量
(推荐,清晰表明属于类)。 - 通过对象:
对象名.静态成员变量
或对象指针->静态成员变量
(不推荐,易误解为对象成员)。
MyClass obj;
obj.s_count = 10; // 合法,但不推荐
MyClass::s_count = 20; // 推荐方式
初始化规则
- 非 const 静态成员:无论类型(整型、指针、类类型),非
const
静态成员必须在类外定义(除非是constexpr
类型)。 - const 静态成员(C++11 起):
- 类型:
int
、char
、long
、short
、bool
、枚举(enum
)等整型家族类型,可在类内直接初始化。任意指针类型(如const char*
、int*
、自定义类型指针)不允许在类内直接初始化。
class MyClass { public:static const int s_max = 100; // 类内初始化(合法)static const char* s_name; // 类内不可初始化,需类外定义 }; const char* MyClass::s_name = "Static Member"; // 类外定义
- 类型:
作用与场景
- 计数器:记录类的实例化次数。
class Counter { public:Counter() {++s_instanceCount; }~Counter() {--s_instanceCount; }static int getInstanceCount() {return s_instanceCount; }private:static int s_instanceCount; // 静态成员变量记录实例数 };int Counter::s_instanceCount = 0; // 类外初始化int main() {Counter a, b;std::cout << Counter::getInstanceCount(); // 输出 2return 0; }
- 共享配置:存储类的全局配置(如日志级别、数据库连接参数)。
- 单例模式:作为单例类的唯一实例(静态成员变量指向自身)。
注意事项
- 访问权限:静态成员仍受
public
/private
/protected
控制。- 私有静态成员只能在类内或友元函数中访问。
- 模板类的静态成员:每个模板特化实例拥有独立的静态成员。
template <typename T> class TemplateClass { public:static T s_value; };template <typename T> T TemplateClass<T>::s_value; // 模板类静态成员需在类外特化定义
- 初始化顺序:
- 静态成员变量的初始化顺序与全局变量类似,按定义顺序在 main () 前初始化,跨编译单元的初始化顺序未定义(可能导致问题)。
2. 静态成员函数(Static Member Functions)
基本概念
- 声明与调用:
- 在类内用
static
声明,无this
指针(不依赖对象)指定类型和访问限定符就可以访问。
class MyClass { public:static void staticFunc() {// 只能访问静态成员或全局变量// 不能访问非静态成员(无this指针)} };int main() {MyClass::staticFunc(); // 通过类名调用(推荐)MyClass obj;obj.staticFunc(); // 通过对象调用(合法但不推荐)return 0; }
- 在类内用
- 特性:
- 不能声明为
virtual
(无多态性,因不依赖对象)。 - 不能使用
const
修饰(无this
指针可供保护)。
- 不能声明为
限制与用途
- 只能访问静态成员:
- 静态函数中不能直接访问非静态成员变量或非静态成员函数,除非通过对象显式调用。
class MyClass { public:int m_data;static void staticFunc(MyClass& obj) {obj.m_data = 10; // 合法,通过对象访问非静态成员} };
- 工具方法:
- 作为不依赖对象状态的工具函数(如工厂方法、辅助函数)。
#include <iostream>// 定义 MathUtils 类 class MathUtils { public:// 静态成员函数,用于执行加法操作static float Add(float a, float b) {return a + b;} };// 定义一个接受回调函数指针的函数 // 该函数会调用传入的回调函数来完成特定的计算 float performCalculation(float x, float y, float (*callback)(float, float)) {return callback(x, y); }int main() {float num1 = 5.0f;float num2 = 3.0f;// 将 MathUtils 类的静态成员函数 Add 作为回调函数传递给 performCalculation 函数float result = performCalculation(num1, num2, MathUtils::Add);std::cout << "计算结果是: " << result << std::endl;return 0; }
- 回调函数:
- 静态成员函数可作为 C 风格回调函数(普通成员函数因含
this
指针无法直接作为回调)。
- 静态成员函数可作为 C 风格回调函数(普通成员函数因含
代码解释
-
MathUtils
类:Add
是一个静态成员函数,它接收两个float
类型的参数a
和b
,然后返回它们的和。因为是静态成员函数,所以可以不依赖于类的对象来调用。
-
performCalculation
函数:- 这个函数接收三个参数:两个
float
类型的数值x
和y
,以及一个函数指针callback
。 - 函数指针
callback
指向一个接受两个float
类型参数并返回float
类型结果的函数。 - 在函数内部,调用
callback
函数,并传入x
和y
作为参数,最后返回计算结果。
- 这个函数接收三个参数:两个
-
main
函数:- 定义了两个
float
类型的变量num1
和num2
,分别初始化为5.0f
和3.0f
。 - 调用
performCalculation
函数,将num1
、num2
以及MathUtils::Add
作为参数传递进去。这里MathUtils::Add
就是作为回调函数使用的。 - 将
performCalculation
函数的返回值赋给result
变量,并将结果输出到控制台。
- 定义了两个
普通成员函数不能直接作为回调函数的原因
普通成员函数包含一个隐式的 this
指针,这个指针指向调用该成员函数的对象。而 C 风格的回调函数是不包含 this
指针的,所以普通成员函数没办法直接当作回调函数使用。静态成员函数不依赖于类的对象,也就没有 this
指针,因此可以作为 C 风格回调函数。
与普通成员函数的区别
特性 | 静态成员函数 | 普通成员函数 |
---|---|---|
this 指针 | 无 | 有(指向当前对象) |
访问非静态成员 | 不能(除非通过对象) | 可以 |
多态性 | 不支持(不能是虚函数) | 支持(可声明为虚函数) |
调用方式 | 类名::函数 或 对象。函数 | 对象。函数 或 指针 -> 函数 |
注意事项
- 隐藏父类静态成员:
- 子类定义同名静态成员会隐藏父类的静态成员(非覆盖,因静态成员属于类,与多态无关)。
#include <iostream> #include <thread> #include <mutex>// 父类 class Parent { public:static void func() {std::cout << "Parent::func() is called." << std::endl;}// 定义一个静态成员变量,模拟共享资源static int sharedResource;// 定义一个互斥量用于保护共享资源static std::mutex mtx; };// 初始化父类的静态成员变量 int Parent::sharedResource = 0; std::mutex Parent::mtx;// 子类 class Child : public Parent { public:static void func() {std::cout << "Child::func() is called." << std::endl;}// 多线程操作共享资源的函数static void incrementSharedResource() {for (int i = 0; i < 1000; ++i) {// 加锁,确保线程安全std::lock_guard<std::mutex> lock(mtx);++sharedResource;}} };int main() {// 调用子类隐藏的父类静态成员函数Child::Parent::func();// 调用子类的静态成员函数Child::func();// 创建多个线程来操作共享资源std::thread t1(Child::incrementSharedResource);std::thread t2(Child::incrementSharedResource);// 等待线程执行完毕t1.join();t2.join();// 输出共享资源的最终值std::cout << "Final value of sharedResource: " << Parent::sharedResource << std::endl;return 0; }
代码解释:
-
子类隐藏父类静态成员:
Parent
类和Child
类都定义了同名的静态成员函数func()
。- 在
main
函数中,通过Child::Parent::func()
显式调用父类的静态成员函数,而Child::func()
则调用子类的静态成员函数,这体现了子类对父类静态成员的隐藏。
-
多线程安全访问静态成员:
Parent
类定义了一个静态成员变量sharedResource
作为共享资源,以及一个静态的std::mutex
对象mtx
用于保护该资源。Child
类的incrementSharedResource
函数用于对sharedResource
进行递增操作。在操作之前,使用std::lock_guard<std::mutex>
加锁,确保同一时间只有一个线程可以访问和修改sharedResource
,从而保证了线程安全。- 在
main
函数中,创建了两个线程t1
和t2
来调用incrementSharedResource
函数,模拟多线程并发访问共享资源的情况。最后等待两个线程执行完毕,并输出sharedResource
的最终值。
通过这个示例,你可以清楚地看到子类如何隐藏父类的静态成员,以及如何使用互斥量来保证静态成员在多线程环境下的安全访问。
3. 静态成员 vs 全局变量 / 函数
特性 | 静态成员 | 全局变量 / 函数 |
---|---|---|
作用域 | 类作用域(避免命名污染) | 全局作用域 |
访问控制 | 支持 public /private | 无(依赖命名空间) |
与类的关联 | 属于类,逻辑上更内聚 | 独立于类 |
面向对象封装 | 支持(类的一部分) | 不支持 |
4. 总结
- 静态成员变量:
- 共享类级别的数据,所有对象可见,需在类外初始化。
- 用于计数器、配置信息、单例实例等场景。
- 静态成员函数:
- 无
this
指针,操作静态成员或全局数据,用于工具方法、回调函数等。
- 无
- 最佳实践:
- 通过类名访问静态成员,避免依赖对象实例。
- 合理设计访问权限,确保封装性。
- 注意跨编译单元的静态成员初始化顺序问题。
静态成员是 C++ 中实现类级别共享和工具功能的核心机制,合理使用可提高代码的组织性和效率,但需注意其与对象状态的解耦特性及潜在的初始化问题。
四、友元:打破封装的 “特殊通行证”
在 C++ 中,封装是面向对象编程的重要特性之一,它通过访问控制(如 private
、protected
、public
)来隐藏类的内部实现细节,保证数据的安全性和完整性。然而,在某些特定情况下,我们可能需要让外部的函数或类能够访问某个类的私有成员,这时就可以使用友元机制。友元就像是一张 “特殊通行证”,可以打破类的封装限制。下面分别对友元函数和友元类进行详细讲解。
1. 友元函数:外部函数访问私有成员
1.1 基本概念
友元函数是一种特殊的函数,它虽然不是类的成员函数,但却可以访问该类的私有和保护成员。友元函数的声明需要在类的定义中使用 friend
关键字。
1.2 示例代码
#include <iostream>class Rectangle {
private:double length;double width;
public:Rectangle(double l, double w): length(l), width(w) {}// 声明友元函数friend double calculateArea(const Rectangle& rect);
};// 友元函数的定义
double calculateArea(const Rectangle& rect) {// 可以访问 Rectangle 类的私有成员return rect.length * rect.width;
}int main() {Rectangle rect(5.0, 3.0);double area = calculateArea(rect);std::cout << "The area of the rectangle is: " << area << std::endl;return 0;
}
1.3 代码解释
- 在
Rectangle
类中,length
和width
是私有成员变量,外部函数通常无法直接访问它们。 - 通过在
Rectangle
类中使用friend double calculateArea(const Rectangle& rect);
声明calculateArea
为友元函数,该函数就获得了访问Rectangle
类私有成员的权限。 - 在
main
函数中,创建了一个Rectangle
对象rect
,并调用calculateArea
函数计算其面积。
1.4 注意事项
- 友元函数不属于类的成员:友元函数没有
this
指针,它的调用方式和普通函数一样,不需要通过对象来调用。 - 友元关系是单向的:如果
A
类声明B
函数为友元函数,并不意味着B
函数所在的类(如果有)或其他函数也能访问A
类的私有成员。 - 友元关系不具有传递性:如果
A
类声明B
函数为友元函数,B
函数所在的类又声明C
函数为友元函数,C
函数并不能访问A
类的私有成员。
2. 友元类:整个类的成员函数都是友元
2.1 基本概念
友元类是指一个类可以将另一个类声明为自己的友元,这样,被声明为友元的类的所有成员函数都可以访问该类的私有和保护成员。同样,友元类的声明也需要在类的定义中使用 friend
关键字。
2.2 示例代码
#include <iostream>class Rectangle {
private:double length;double width;
public:Rectangle(double l, double w)]: length(l), width(w) {}// 声明友元类friend class AreaCalculator;
};class AreaCalculator {
public:double calculateArea(const Rectangle& rect) {// 可以访问 Rectangle 类的私有成员return rect.length * rect.width;}
};int main() {Rectangle rect(5.0, 3.0);AreaCalculator calculator;double area = calculator.calculateArea(rect);std::cout << "The area of the rectangle is: " << area << std::endl;return 0;
}
2.3 代码解释
- 在
Rectangle
类中,使用friend class AreaCalculator;
声明AreaCalculator
为友元类。 AreaCalculator
类的calculateArea
函数可以直接访问Rectangle
类的私有成员length
和width
。- 在
main
函数中,创建了Rectangle
对象rect
和AreaCalculator
对象calculator
,并调用calculator
的calculateArea
函数计算rect
的面积。
2.4 注意事项
- 友元类的成员函数都具有访问权限:一旦一个类被声明为另一个类的友元类,该类的所有成员函数都可以访问另一个类的私有和保护成员。
- 友元类的声明位置:友元类的声明可以放在类的任何位置(
private
、protected
或public
部分),效果是一样的。 - 友元关系的单向性和非传递性同样适用:友元类的关系是单向的,不具有传递性。
- 友元函数不能用const修饰
- 一个函数可以是多个类的友元函数
3. 友元机制的优缺点
3.1 优点
- 提高代码的灵活性:在某些情况下,友元机制可以让我们更方便地实现一些功能,例如运算符重载、数据结构的遍历等。
- 简化代码:通过友元函数或友元类,可以避免为了访问私有成员而提供过多的公共接口,从而简化代码。
3.2 缺点
- 破坏封装性:友元机制打破了类的封装性,使得类的私有成员可以被外部访问,这可能会导致数据的安全性和完整性受到威胁。
- 增加代码的耦合度:使用友元机制会增加类之间的耦合度,使得代码的维护和扩展变得更加困难。
因此,在使用友元机制时,需要谨慎权衡其优缺点,只在必要的情况下使用。
五、内部类:类中的 “嵌套小世界”
1. 定义与特点
定义
想象一下,你有一个大盒子(外部类),在这个大盒子里面又放了一个小盒子(内部类)。在 C++ 里,就是在一个类的内部再定义一个类,这个在内部定义的类就是内部类。下面是一个简单的例子:
class School { // 这是外部类,就像大盒子
public:class Classroom { // 这是内部类,就像大盒子里的小盒子public:void teach() {// 这里是内部类的一个行为,比如上课}};
};
在这个例子中,School
是外部类,Classroom
是内部类。
特点
- 作用域受限:内部类就像被关在大盒子里的小盒子,它的活动范围被限制在外部类里面。你在外面想用这个小盒子,就得先通过大盒子找到它。例如,要创建
Classroom
的对象,就得这样写:
School::Classroom room;
room.teach();
这里的 School::Classroom
就是通过大盒子(School
)找到了小盒子(Classroom
)。
- 访问权限:
- 内部类和外部类的访问权限是各自独立的。内部类可以直接访问外部类的静态成员,静态成员就像是大盒子里大家都能看到的公共物品。比如:
class School {
private:static int schoolId; // 这是外部类的静态私有成员,像公共物品
public:class Classroom {public:void setSchoolId() {schoolId = 1; // 内部类可以直接访问外部类的静态私有成员}};
};
int School::schoolId = 0;
- 但是内部类不能直接访问外部类的非静态成员,非静态成员就像是大盒子主人自己的东西,小盒子里的东西不能直接拿。不过,如果内部类拿到了大盒子主人的钥匙(外部类对象),就可以访问了。
- 封装性好:内部类可以把和外部类紧密相关的功能藏起来,就像把小盒子里的东西藏好,不让外面随便看到,这样能避免和外面的东西搞混,减少麻烦。
- 独立性:虽然内部类在外部类里面,但它自己是个独立的个体,有自己的成员变量、成员函数,还有自己的访问规则,就像小盒子有自己的东西和管理方式。
2. 访问方式
外部类访问内部类
外部类要访问内部类的东西,就得先把小盒子打开,也就是创建内部类的对象。例如:
class School {
public:class Classroom {public:int studentCount;void startClass() {// 开始上课}};void useClassroom() {Classroom classRoom; // 创建内部类对象,打开小盒子classRoom.studentCount = 30; // 访问内部类的成员变量classRoom.startClass(); // 访问内部类的成员函数}
};
在 School
类的 useClassroom
函数里,创建了 Classroom
对象,然后就可以用它里面的东西了。
内部类访问外部类
内部类可以直接拿大盒子里的公共物品(外部类的静态成员)。要是想拿大盒子主人自己的东西(外部类的非静态成员),就得有主人的钥匙(外部类对象)。例如:
class School {
private:static int schoolYear; // 外部类的静态成员,公共物品int teacherCount; // 外部类的非静态成员,主人自己的东西
public:class Classroom {public:void accessSchool(School& school) {schoolYear = 2024; // 直接访问外部类的静态成员school.teacherCount = 10; // 通过外部类对象访问非静态成员}};
};
int School::schoolYear = 0;int main() {School school;School::Classroom classRoom;classRoom.accessSchool(school);return 0;
}
在 Classroom
类的 accessSchool
函数里,通过传入 School
对象,就可以访问外部类的非静态成员了。
外部访问内部类
在外面想用小盒子里的东西,就得先通过大盒子找到小盒子,也就是用外部类的作用域解析运算符 ::
。例如:
class School {
public:class Classroom {public:void showInfo() {std::cout << "This is a classroom." << std::endl;}};
};int main() {School::Classroom classRoom; // 通过大盒子找到小盒子classRoom.showInfo(); // 使用小盒子里的东西return 0;
}
在 main
函数里,通过 School::Classroom
创建了内部类对象,然后调用了它的成员函数。
3. 注意事项
- 嵌套别太深:内部类可以一层套一层,就像大盒子里有小盒子,小盒子里还能有更小的盒子。但是套太多层,自己都会迷糊,代码的可读性和可维护性就变差了。
- 生命周期独立:内部类对象的存在和消失和外部类对象没有关系,就像小盒子里的东西什么时候拿走,和大盒子什么时候扔掉没关系。
- 友元关系:外部类可以给内部类一把特殊的钥匙(友元关系),这样内部类就能随便拿大盒子主人自己的东西(访问外部类的私有成员)了。例如:
class School {
private:int schoolSecret; // 外部类的私有成员,主人藏起来的东西friend class Classroom; // 给 Classroom 类一把特殊钥匙
public:class Classroom {public:void knowSecret(School& school) {school.schoolSecret = 123; // 可以访问外部类的私有成员}};
};
这里 Classroom
类成了 School
类的友元类,就能访问 School
类的私有成员 schoolSecret
了。
- sizeof(外部类)=外部类,和内部类没有任何关系 除非定义里面的类。列如:
从以上我们可以看出如果没有在A类中定义B类是不会计算B类的大小的。
六、匿名对象:“一次性” 的临时帮手
1. 定义与生命周期
定义
在 C++ 里,匿名对象就是那种没有名字的对象。想象一下,你在生活中有时候需要某个东西,只用一次,用完就扔,没必要专门给它取个名字。在编程里也是,当你只需要临时用一下某个对象,不想给它分配一个变量名来长期保存时,就可以创建匿名对象。
比如下面这个简单的类:
class Dog {
public:Dog() {cout << "给狗取名字" << endl;}void bark() {cout << "汪!汪!" << endl;}~Dog() {cout << "狗到生命结束." << endl;}
};
创建匿名对象的方式就是直接调用类的构造函数,像这样:
Dog();//无名
Dog d1;//有名字
这里的 Dog()
就是创建了一个匿名的 Dog
对象。
生命周期
匿名对象的生命周期非常短暂,它就像一个 “一次性” 的临时帮手,只用一次就会被销毁。具体来说,匿名对象的生命周期从创建开始,到包含它的语句执行结束就结束了。
看下面这个例子:
#include <iostream>
class Dog {
public:Dog() {cout << "给狗取名字" << endl;}void bark() {cout << "汪!汪!" << endl;}~Dog() {cout << "狗到生命结束。" << endl;}
};int main() {Dog().bark();cout << "这一行是在匿名狗狗使用之后。" << endl;return 0;
}
在 main
函数里,Dog().bark()
这一句先创建了一个匿名的 Dog
对象,然后调用它的 bark
方法。当这一句执行完,匿名对象的使命就完成了,它的析构函数会被调用,对象被销毁。所以输出结果会是:
给狗取名字.
汪汪!汪汪
狗到生命结束。
这一行在匿名狗使用之后。
2. 用途
作为函数参数
匿名对象可以直接作为函数的参数传递,这样可以避免创建一个临时的命名对象,让代码更简洁。
假设有一个函数用来喂狗:
void feedDog(const Dog& dog) {cout << "喂狗..." << endl;
}
在调用这个函数时,可以直接传递一个匿名对象:
feedDog(Dog());
这里创建了一个匿名的 Dog
对象并传递给 feedDog
函数,函数执行完后,匿名对象就被销毁了。
初始化对象
匿名对象可以用来初始化其他对象。比如有一个 Cat
类,并且有一个接受 Cat
对象的构造函数:
class Cat {
public:Cat() {std::cout << "给猫取名字" << std::endl;}Cat(const Cat& other) {cout << "猫生下一个跟自己一样的" << endl;}~Cat() {std::cout << "猫到生命结束" << std::endl;}
};int main() {Cat myCat = Cat();return 0;
}
在这个例子中,先创建了一个匿名的 Cat
对象,然后用它来初始化 myCat
对象。这里可能会发生拷贝构造或者编译器的优化(如拷贝省略),但不管怎样,匿名对象在初始化完成后就会被销毁。
调用类的静态成员函数
如果类有静态成员函数,也可以用匿名对象来调用。静态成员函数属于类本身,不依赖于具体的对象。
class MathUtils {
public:static int add(int a, int b) {return a + b;}
};int result = MathUtils().add(3, 5);
这里通过匿名的 MathUtils
对象调用了静态成员函数 add
,计算出 3 和 5 的和。
总之,匿名对象在 C++ 里是一种很有用的特性,它能让代码更简洁,在只需要临时使用对象的场景下非常方便。但要注意它的生命周期很短,使用完就会被销毁。
七、对象拷贝优化:编译器的 “偷工减料” 技巧
1. 优化场景
- 传值传参:
f1(1)
会隐式构造 A 对象,编译器优化为直接构造,省去拷贝。 - 传值返回:函数返回局部对象时,编译器可能将返回值直接构造到接收对象中,避免临时对象拷贝。
2. 关闭优化(调试用)
- g++ 编译时加参数
-fno-elide-constructors
,可查看真实拷贝次数。
总结:进阶特性的 “双刃剑”
- 初始化列表:必须掌握,尤其是引用 /const/ 非默认构造成员的初始化。
- static 成员:用于全局统计或工具函数,类外初始化别忘记。
- 友元与内部类:突破封装但需谨慎,避免滥用破坏代码结构。
- 匿名对象与拷贝优化:理解生命周期和编译器行为,写出高效代码。
通过这些特性,C++ 在封装性和灵活性之间找到平衡,但记住:合适的才是最好的,复杂特性请按需使用!