引言

在C++编程中,类与对象是重要的概念,但是有一些高级特性需要更深入的了解。本篇博客将介绍四个主题:初始化列表static成员友元匿名对象。这些特性可以让我们更加灵活地设计和使用类与对象,提高代码的效率和可维护性。

一、 初始化列表

1.1 构造函数内部赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Date {
public:
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}

private:
int _year;
int _month;
int _day;
};

在上述构造函数调用后,对象中的成员变量会获得初始值,但要严格区分,构造函数体中的语句只能被称为赋初值,而不能被称为初始化。这是因为初始化只能对象创建时进行一次,而构造函数体内的语句可以在构造函数执行过程中多次赋值。因此,我们将要学习的初始化列表是实现对象成员变量真正初始化的有效方式

1.2 使用初始化列表

初始化列表是C++中用于在构造函数中初始化类成员变量的一种机制。它允许在构造函数的参数列表之后,通过冒号(:)来显式地对成员变量进行初始化,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。这种方式在对象创建时仅执行一次,相比在构造函数体内使用赋值语句初始化成员变量,更加高效和推荐

初始化列表的语法如下:

1
2
3
ClassName::ClassName(parameters) : member1(value1), member2(value2), ..., memberN(valueN) {
// 构造函数体
}

其中,ClassName 是类的名称,parameters 是构造函数的参数列表,而 member1, member2, …, memberN 是类的成员变量,value1, value2, …, valueN 是成员变量对应的初始值。

1.3 注意事项

每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。

对于const或引用类型的成员变量,初始化列表是唯一的初始化方式

因为在构造函数内部是进行赋值,而此处的两种对象只能进行初始化,之后不能再进行修改,因此只能使用初始化列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass {
private:
int x;
const int y;
double& z;
public:
// 初始化列表在这里进行成员变量的初始化
MyClass(int a, int b, double& c) : x(a), y(b), z(c) {
// 构造函数体
}
};

int main() {
int num = 42;
double val = 3.14;
MyClass obj(10, num, val);
// obj.x 被初始化为 10
// obj.y 被初始化为 num (42)
// obj.z 被初始化为 val (3.14)
return 0;
}

③ 自定义类型成员(且该类没有默认构造函数时)也必须使用初始化列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public:
A(int a)
: _a(a) {}

private:
int _a;
};

class B {
public:
B(int a, int ref)
: _aobj(a) {}

private:
A _aObj; // 没有默认构造函数
}

若此时不使用初始化列表,实例化B类对象时将无法创建,因为在实例化时调用B构造函数,A类成员变量没有合适的默认构造函数,只有在初始化列表中显示使用有参构造函数才行。

④成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A {
public:
A(int a)
: _a1(a), _a2(_a1) {
}

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

private:
int _a2;
int _a1;
};

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

A. 输出1 1 B.程序崩溃 C.编译不通过 D.输出1 随机值

答案是D。

解析:

我们看到初始化列表是先对成员变量_a1先进行初始化,再初始化_a2,但是实际上初始化成员变量的顺序只与在类中的声明顺序有关!!!我们先声明的_a2,因此在初始化列表中,先对_a2进行初始化,但是此时_a1的值是随机值,因为类对其中内置类型是默认不做处理的,所以其值为随机值并初始化_a2,其次在使用形参a初始化_a1。由此我们能得到答案D。

⑤ 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化。

上面这段话的意思是,当我们实例化一个类对象时,不管我们是否使用初始化列表对成员变量进行赋值,编译器都会先使用初始化列表进行初始化。那么问题来了,我不调用它都会自动使用,那么会造成什么结果呢?自动使用的结果是:内置成员变量的值为随机值不做处理,类类型的成员变量使用默认构造函数初始化。

当我们在函数体内操作时,只能是对其进行赋值操作,而不是初始化,如果要使用类类型的有参构造函数等,一定得使用初始化列表!!!还有上面提到的必须使用初始化列表的情况。

还有就是初始化列表初始化与函数体内赋值可以混合进行,不过要注意的是,不管成员变量有没有显示使用,都会经过初始化列表。

总之建议最好使用初始化列表对变量进行初始化!!!

1.4 explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用。

来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass {
public:
MyClass(int x) : _x(x) {}

private:
int _x;
};

int main() {
MyClass obj1 = 42; // 隐式类型转换
MyClass obj2(42); // 使用显式构造函数进行对象的创建

return 0;
}

是不是对obj1的实例化有有点疑惑?

一个int类型对象怎么能够和类类型对象对等呢??这和我们的认知不太一样啊!

我来介绍一下此处进行的隐式类型转换:

一般隐式类型转换原理上都会产生临时变量,此处先使用构造函数初始化一个匿名临时变量MyClass(42)(现在读者仅需了解此处有匿名对象即可),再使用默认生成的拷贝构造函数初始化obj1,便是我们看到的代码,不过这种方式效率比较低,因此现代大多是编译器会对过程进行优化,只调用一次构造函数直接实例化obj1对象,将42传给形参x实例化。

我们来对其进行验证:

1
2
有参构造函数
有参构造函数

这是上述代码的运行结果,和上述解释的一致。

还有一种有参构造函数也能算此种情况,那就是除第一个参数无默认值其余均有默认值的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

class MyClass {
public:
MyClass(int x, ibt y = 1) : _x(x), _y(y) {
std::cout << "有参构造函数" << std::endl;
}
private:
int _x;
int _y;
};

int main() {
MyClass obj1 = 42; // 隐式类型转换
MyClass obj(42); // 使用显示构造函数
retuen 0;
}

此段代码的原理和上述情况是相同的。

由此我们引入explicit关键字

explicit 是C++中的一个关键字,用于修饰单参数构造函数,它的作用是防止编译器进行隐式类型转换。通常情况下,当我们在构造函数中只有一个参数时,编译器会自动进行类型转换,将该参数类型转换为类的对象,从而创建临时对象。使用 explicit 关键字可以阻止这种隐式的自动类型转换。

使用 explicit 关键字可以避免一些意外的类型转换,增强了代码的可读性和安全性。它通常用于那些希望明确声明只接受显式构造函数调用的类。

二、 static成员

2.1 概念

static 是C++中的一个关键字,用于定义静态成员变量和静态成员函数。静态成员与类本身相关联,而不是与类的对象相关联。这意味着静态成员在类的所有对象之间共享,而不是每个对象都有自己的副本。静态成员在整个类的生命周期中存在,直到程序结束

2.2 情景

问题:实现一个类,并计算程序中创建出了多少个类对象

思路:我们可以定义全局变量并在构造函数内对其进行递增!

弊端:程序其他地方可能会对此全局变量进行修改!

那么让我们看一看正确思路(读者先见一见使用方法即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

class Counter {
public:
Counter() {
count++; // 每次创建对象时递增计数
}

static int getCount() {
return count;
}

private:
static int count; // 静态成员变量用于计数
};

int Counter::count = 0; // 静态成员变量初始化

int main() {
Counter obj1;
Counter obj2;
Counter obj3;

std::cout << "Number of objects created: " << Counter::getCount() << std::endl; // 输出:Number of objects created: 3

return 0;
}

在上面的示例中,我们创建了一个名为 Counter 的类,它具有一个静态成员变量 count 和一个构造函数。每当对象被创建时,构造函数会自动调用,并在其中递增 count。这样,每次创建对象时,就会自动计算已创建的对象数量。

main 函数中,我们创建了三个 Counter 对象,然后通过 Counter::getCount() 静态成员函数获取已创建的对象数量,并输出结果。输出结果为 3,表示程序中一共创建了三个 Counter 对象。

我们在类中声明了一个静态成员变量count并在类外进行初始化,实例中仅有默认构造函数,在实例化对象时构造函数中的count变量++,静态变量是属于类的,而不是类对象,静态成员在整个类的生命周期中存在,直到程序结束。其中getCount函数为静态成员函数,通过调用来获取类声明对象的个数。

解释一下,静态成员变量和函数均属于类,不属于对象,静态成员函数中无this指针,他们均可通过类名::变量/函数调用,也可以通过类对象调用。

2.3 特性

  1. 所有对象共享:静态成员变量在类的所有对象之间共享,对于所有对象来说,它们都指向同一个内存位置,存放在静态区。
  2. 类内声明,类外初始化:静态成员变量必须在类的内部进行声明,但是在类外部进行初始化,通常在类外初始化静态成员变量。
  3. 可以通过类名访问:由于静态成员与类相关联,而不是与对象相关联,因此可以通过类名来访问静态成员,而不需要创建对象
  4. 不占用对象空间:由于静态成员是与类相关联的,而不是与对象相关联的,因此它不占用对象的内存空间
  5. 静态成员函数:静态成员函数是一个与类关联而不是与对象关联的函数。它只能访问类的静态成员变量和其他静态成员函数,而不能访问普通成员变量和非静态成员函数,因为它没有this指针。静态成员函数可以通过类名直接调用,无需创建对象。
  6. 静态成员的访问权限:静态成员变量和静态成员函数具有与类的访问权限相同的访问级别。如果一个成员函数是私有的,那么只有类的成员函数可以访问它,即使是静态成员函数,不能通过类名调用。
  7. 静态成员和动态内存分配:静态成员变量不占用对象的内存空间,因此它不会影响对象的大小。但是,如果静态成员变量是指向动态分配内存的指针,那么这个指针指向的内存块是位于堆上的,因此需要手动释放,否则可能导致内存泄漏。
  8. 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
  9. 静态成员函数不可以调用非静态成员函数,因为其中并没this指针,非静态成员函数可以调用类的静态成员函数。

三、 友元

3.1 概念

友元(friend)是C++中的一个特性,它允许一个类将其他类或非成员函数声明为自己的友元,从而允许这些友元访问该类的私有成员和保护成员,友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

3.2 语法

友元声明语法是在类的内部进行的,通过在类中声明其他类或函数为友元来建立友元关系。友元声明使用 friend 关键字。

1
2
3
4
5
6
7
8
class ClassName {
public:
// 成员函数和成员变量的声明

friend ReturnType FriendFunctionName(Parameters); // 友元函数的声明

friend class FriendClassName; // 友元类的声明
};

通过两个示例来详细解释友元的使用

3.2.1 友元函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

class A {
public:
A(int value) : _a(value) {}

// 友元函数声明
friend void showAValue(const A &objA);

private:
int _a;
};

// 友元函数定义,访问类A的私有成员
void showAValue(const A &objA) {
std::cout << "A's value: " << objA._a << std::endl;
}

int main() {
A objA(10);

// 调用友元函数,访问类A的私有成员
showAValue(objA); // 输出:A's value: 10
return 0;
}

在上述示例中,我们定义了类A。在类A中声明了一个友元函数 showAValue,允许该函数访问类A的私有成员 _a

main 函数中,我们创建了类A的对象 objA ,然后调用 showAValue 函数访问类A的私有成员。

3.2.2 友元类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>

class A; // 前向声明类A,用于在类B中声明友元类

class B {
public:
B(int value) : _b(value) {}

void showAValue(const A& objA); // 类A的引用作为参数

private:
int _b;
};

class A {
public:
A(int value) : _a(value) {}
friend class B; // 声明B为A的友元类

private:
int _a;
};

void B::showAValue(const A& objA) {
std::cout << "A's value: " << objA._a << std::endl; // 在B的成员函数中访问A的私有成员
}

int main() {
A objA(10);
B objB(20);

objB.showAValue(objA); // 输出:A's value: 10

return 0;
}

在上述示例中,我们定义了两个类:类A和类B。在类B中声明了一个友元函数 showAValue,它的参数是类A的引用。在类A中将类B声明为友元类,这样 showAValue 函数可以访问类A的私有成员 _a

main 函数中,我们创建了类A的对象 objA 和类B的对象 objB,然后调用 showAValue 函数,输出了类A的私有成员 _a 的值。由于 showAValue 函数是类B的成员函数,并且在类A中将类B声明为友元类,因此它可以访问类A的私有成员。

3.3 特性

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

解释一下第二点

友元函数不能使用 const 修饰符。在C++中,友元函数是独立于类的非成员函数,它虽然可以访问声明为友元的类的私有成员和保护成员,但不能被声明为 const 成员函数。

const 成员函数是指在类中被声明为 const 的成员函数,它表示该成员函数不会修改类的成员变量(mutable 成员变量)。这样的成员函数可以在 const 类对象上调用,以保证对象的状态不会被修改。

因为友元函数不属于类的成员函数,所以它没有 constvolatile 修饰符。友元函数不受限于类对象的 const 语义,因此它可以修改类的私有成员和保护成员,不受 const 限制。

四、匿名对象

4.1 概念

匿名对象是在C++中创建的一个没有命名的临时对象,它不被赋予任何变量名,只能在创建的表达式中使用一次,之后就会立即销毁。

匿名对象通常用于简化代码和临时的计算,它们不需要被命名,因为它们只在创建它们的表达式中存在,并且在表达式结束后就会被销毁,从而节省了内存和资源。

4.2 语法

匿名对象的语法非常简单,它是在创建对象时省略对象的命名,直接使用临时对象。匿名对象只能在创建的表达式中使用一次,之后就会立即被销毁。

1
ClassType(); // 创建匿名对象

在上面的语法中,ClassType 是要创建的类的名称,后面的括号表示调用类的构造函数来创建对象。

4.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

class Point {
public:
Point(int x, int y) : _x(x), _y(y) {}

int getX() const { return _x; }
int getY() const { return _y; }

private:
int _x;
int _y;
};

void displayPoint(const Point& p) {
std::cout << "Point: (" << p.getX() << ", " << p.getY() << ")" << std::endl;
}

int main() {
// 创建匿名对象并直接传递给函数
displayPoint(Point(2, 3));

// 作为函数返回值
Point p1(1, 1);
Point p2(2, 2);
Point sum = Point(p1.getX() + p2.getX(), p1.getY() + p2.getY());
displayPoint(sum);

// 临时计算
int result = Point(4, 5).getX() + Point(6, 7).getY();
std::cout << "Result: " << result << std::endl;

return 0;
}

在上述示例中,我们定义了一个 Point 类,表示二维坐标点。然后在 main 函数中,我们使用匿名对象在不同的情况下进行了演示:

  1. displayPoint 函数中,直接创建并传递匿名对象,用于显示点的坐标。
  2. 在计算两个点的和时,使用匿名对象作为函数返回值,而无需定义额外的变量。
  3. 在临时计算中,直接使用匿名对象的成员函数进行运算。

通过匿名对象,我们可以简化代码并避免定义不必要的临时变量,从而提高代码的简洁性和可读性。但要注意,由于匿名对象的生命周期仅限于其所在的表达式,不应在其生命周期之外使用它。

4.4 用途

  1. 作为函数返回值:某些情况下,函数可以返回一个临时的匿名对象,而无需定义一个变量来接收返回值。
  2. 作为函数参数:可以直接将匿名对象作为函数的参数传递,而不需要在调用函数前定义一个变量来存储对象。
  3. 临时计算:在某些表达式中,可以直接使用匿名对象进行计算,而不必显示地命名和定义变量。

五、内部类

5.1 概念

在C++中,内部类是在另一个类的内部声明的类,也被称为嵌套类。即允许在一个类的内部定义另一个类,从而形成一个类的层次结构。内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越 的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义。内部类可以通过外部类的对象参数来访 问外部类中的所有成员。但是外部类不是内部类的友元。

5.2 特性

  1. 内部类可以定义在外部类的publicprotectedprivate区域都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类类对象大小,和内部类没有任何关系。

5.3 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>

class OuterClass {
private:
int outer_private_member_; // 外部类私有成员

public:
class InnerClass {
public:
// 显示外部类的私有成员
void display(const OuterClass& obj) {
std::cout << "内部类访问外部私有成员:" << obj.outer_private_member_ << std::endl;
}
};

OuterClass(int value) : outer_private_member_(value) {} // 外部类构造函数

// 外部类的显示函数,创建内部类对象并显示外部私有成员
void display() {
InnerClass inner;
inner.display(*this);
}
};

int main() {
OuterClass outer(5); // 创建外部类对象
outer.display(); // 调用外部类的显示函数
return 0;
}

5.4 用途

  • 实现某个类的辅助功能,但这些功能不适合作为独立的类存在。
  • 实现一种特定于外部类的数据结构或算法。
  • 将一组相关的类组织在一起,以提高代码的结构性和可读性。