目录
一、初始化列表
1.1 - 定义
1.2 - 使用初始化列表的原因
1.3 - 成员变量的初始化顺序
二、静态成员
2.1 - 静态成员变量
2.2 - 静态成员函数
三、友元
3.1 - 友元函数
3.2 - 友元类
四、内部类
五、匿名对象
5.1 - 匿名对象的特性
5.2 - 匿名对象的使用场景
六、编译器所做的一些优化
参考资料:
C++中的初始化列表详解。
C++ 类构造函数初始化列表 | 菜鸟教程 (runoob.com)。
C++ explicit 关键字 - 知乎 (zhihu.com)。
C++ static静态成员变量详解 (biancheng.net)。
C++ static静态成员函数详解 (biancheng.net)。
C++友元函数和友元类(C++ friend)详解。
C++11运算符重载详解与向量类重载实例(<<,>>,+,-,*等)。
C++输入输出(cin和cout) (biancheng.net)。
一、初始化列表
1.1 - 定义
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
class Date
{
public:
// 一、在构造函数体内进行赋值操作
/*Date(int year = 1949, int month = 10, int day = 1)
{
_year = year;
_month = month;
_day = day;
}*/
// 二、使用初始化列表
Date(int year = 1949, int month = 10, int day = 1)
: _year(year), _month(month), _day(day) {}
private:
int _year;
int _month;
int _day;
};
注意,一定要弄清楚初始化(initialization)和赋值(assignment)之间的区别。
在构造函数体内进行的赋值操作不能称作初始化,因为初始化只能进行一次,而赋值可以进行多次。
1.2 - 使用初始化列表的原因
-
性能原因。对于内置类型的成员变量,使用初始化列表和在构造函数体内赋值差别不是很大,但是对于类类型的成员变量来说,最好使用初始化列表。例如:
#include <iostream> using namespace std; class A { public: // 默认的构造函数 A(int x = 0) : _i(x) { cout << "A(int x = 0)" << endl; } // 拷贝构造函数 A(const A& a) : _i(a._i) { cout << "A(const A& a)" << endl; } // 赋值运算符重载 A& operator=(const A& a) { cout << "A& operator=(const A& a)" << endl; _i = a._i; return *this; } void Print() const { cout << _i << endl; } private: int _i; }; class B { public: // 拷贝构造函数 B(const A& a) { _a.Print(); _a = a; _a.Print(); } private: A _a; }; int main() { A a(10); // A(int x = 0) B b(a); // A(int x = 0) // 0 // A& operator=(const A& a) // 10 return 0; }
从概念上来讲,构造函数的执行可以分为两个阶段,初始化阶段和计算阶段,初始化阶段先于计算阶段。
-
在初始化阶段,所有类类型(class type)的成员变量都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中。
-
计算阶段一般用于执行构造函数体内的赋值操作。
所以 B 类中类类型的成员变量
_a
在进入构造函数体之前就已经初始化完成了,即调用了对应的默认构造函数;在进入函数体之后,进行的是对已经初始化的类对象的赋值操作,即调用了对应的赋值运算符重载。class B { public: B(const A& a) : _a(a) { _a.Print(); } private: A _a; };
class B { public: B(int x = 0) : _a(x) { _a.Print(); } private: A _a; };
-
-
除了性能原因之外,有时候初始化列表是不可或缺的。类包含以下成员变量时,必须放在初始化列表位置进行初始化:
const
成员变量,因为const
对象必须初始化。- 引用类型的成员变量,因为引用必须初始化。
- 没有默认构造函数的类类型成员变量,因为使用初始化列表时不必调用默认构造函数来初始化,可以直接调用对应的构造函数来初始化。
#include <iostream> using namespace std; class A { public: // 带参的构造函数 A(int x) : _i(x) { cout << "A(int x)" << endl; } private: int _i; }; class B { public: B(int y, int& r, int x) : _j(y), _r(r), _a(x) {} private: const int _j; // const 成员变量 int& _r; // 引用类型的成员变量 A _a; // 没有默认构造函数的类类型的成员变量 }; int main() { int n = 20; B b(10, n, 30); // A(int x) return 0; }
1.3 - 成员变量的初始化顺序
成员变量在类中声明的次序就是其在初始化列表中初始化的顺序,与其在初始化列表中的先后次序无关。
class A
{
public:
A(int x = 0) : _j(x), _i(_j) {}
void Print() const
{
cout << _i << " " << _j << endl;
}
private:
int _i;
int _j;
};
int main()
{
A a;
a.Print(); // 随机值 0
return 0;
}
二、静态成员
2.1 - 静态成员变量
对象的内存中包含了成员变量,不同的对象占用不同的内存,这使得不同对象的成员变量相互独立,它们的值不受其他对象的影响。可是有时候我们希望在多个对象之间共享数据,在对象 a 改变了某份数据后,对象 b 可以检测到。共享数据的典型使用场景是计数。
在 C++ 中,可以使用静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字 static 修饰。
#include <iostream>
using namespace std;
class A
{
public:
A() { ++_count; }
A(const A& a) { ++_count; }
~A() { --_count; }
static int _count; // 静态成员变量
};
int A::_count = 0;
int main()
{
cout << A::_count << endl; // 0
A a1;
A a2;
A a3(a1);
A a4(a2);
cout << A::_count << endl; // 4
return 0;
}
这段代码定义了一个类 A,其静态成员变量 _count
用来统计当前创建出来的类对象的个数。
static 成员变量的特性:
-
static 成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为
_count
分配一份内存,所有对象使用的都是这份内存中的数据。 -
static 成员变量不占用对象内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问,所以
sizeof(A) == 1
。 -
static 成员变量既可以通过对象来访问,也可以通过类来访问,例如:
A::_count
。 -
static 成员变量必须在类的外部初始化,具体形式为:
type class::name = value;
静态成员变量在初始化时不能再加 static,但必须要有数据类型。被 private、protected、public 修饰的静态成员变量都可以用这种方式初始化。
-
static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在类外初始化时分配,所以没有在类外部初始化的 static 成员变量不能使用。
2.2 - 静态成员函数
在类中,static 除了可以声明静态成员变量,还可以声明静态成员函数。
编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把调用该函数的对象的地址赋值给 this,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。而静态成员函数可以通过类来直接调用,编译器不会为它增加形参 this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数。
普通成员函数可以访问所有成员(包括成员变量和成员函数),而静态成员函数因为没有 this 指针,不知道指向哪个对象,所以无法访问对象的成员变量,即不能访问普通成员变量,也无法调用普通成员函数,只能访问静态成员变量和调用静态成员函数。
#include <iostream>
using namespace std;
class A
{
public:
A() { ++_count; }
A(const A& a) { ++_count; }
~A() { --_count; }
static int GetCount() { return _count; } // 静态成员函数
private:
static int _count; // 静态成员变量
};
int A::_count = 0;
int main()
{
cout << A::GetCount() << endl; // 0
A a1;
A a2;
A a3(a1);
A a4(a2);
cout << A::GetCount() << endl; // 4
return 0;
}
三、友元
私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接地进行。这些固然能带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦。
C++ 设计者认为,如果有的程序员真的非常怕麻烦,就是想在类的成员函数外部直接访问对象的私有成员,那还是做一点妥协以满足他们的愿望为好,这也算是眼前利益和长远利益的折中。因此,C++ 就有了友元(friend) 的概念。打个比方,这相当于是说:朋友是值得信任的,所以可以对他们公开自己的隐私。
友元分为两种:友元函数和友元类。
3.1 - 友元函数
在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为友元,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。注意:友元函数可以在类定义的任何地方声明,不受类访问限定符的限制。
将全局函数声明为友元的写法如下:
friend 返回值类型 函数名(参数列表);
将其他类的成员函数声明为友元的写法如下:
friend 返回值类型 其他类的类名::成员函数名(参数列表);
但是,不能把其他类的私有成员函数声明为友元。
示例:
#include <iostream>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1949, int month = 10, int day = 1)
: _year(year), _month(month), _day(day) {}
private:
int _year;
int _month;
int _day;
};
// 流插入运算符(<<)重载
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day;
return out;
}
// 流提取运算符(>>)重载
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
int main()
{
Date d;
cout << d << endl; // 1949-10-1
cin >> d; // 假设输入:2023 5 1
cout << d << endl; // 2023-5-1
return 0;
}
在编写 C++ 程序时,如果需要使用输入输出时,则需要包含头文件
iostream
,它包含了用于输入输出的对象,例如常见的cin
表示标准输入、cout
表示标准输出,cerr
表示标准错误。
cout
和cin
都是 C++ 的内置对象,而不是关键字。C++ 库定义了大量的类(Class),程序员可以使用它们来创建对象,cout
和cin
分别是ostream
类和istream
类的对象,只不过它们是由标准库的开发者提前创建好的,可以直接拿来使用。这种在 C++ 中提前创建好的对象称为内置对象。无法将 << 和 >> 运算符重载为成员函数:
#include <iostream> using namespace std; class Date { public: Date(int year = 1949, int month = 10, int day = 1) : _year(year), _month(month), _day(day) {} // 流插入运算符(<<)重载 ostream& operator<<(ostream& out) { out << _year << "-" << _month << "-" << _day; return out; } private: int _year; int _month; int _day; }; int main() { Date d; d << cout; // 1949-10-1 // 等价于: // d.operator<<(cout); return 0; }
因为成员函数的第一个参数一定是隐藏的 this 指针,所以对象 d 必须放在 << 的左侧,显然这不符合程序员的使用习惯,违反了直觉。如果将 >> 重载为成员函数也是同样的问题。
3.2 - 友元类
一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员。在类定义中声明友元类的写法如下:
friend class 类名;
示例:
class Time
{
friend class Date;
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour), _minute(minute), _second(second) {}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1949, int month = 10, int day = 1)
: _year(year), _month(month), _day(day) {}
void SetTimeOfDate(int hour, int minute, int second)
{
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d(2023, 5, 1);
d.SetTimeOfDate(12, 30, 00);
return 0;
}
注意:
友元关系是单向的,不具有交换性。以上述的 Time 类和 Date 类例,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类的成员函数中直接访问 Time 类对象的私有成员,但是反过来不行。
友元关系不能传递。如果 B 是 A 的友元,C 是 B 的友元,不能说明 C 是 A 的友元。
友元关系不能继承。
四、内部类
如果一个类定义在另一类的内部,这个内部类就叫作内部类。内部类是一个独立的类,它不属于外部内,更不能通过外部的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类是外部类的友元类,参考友元类的定义,内部类可以通过外部类的对象来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
-
内部类可以定义在外部类的 public、protected、private 都是可以的。
如果内部类定义在 public,则可以通过 外部类名::内部类名 来创建内部类的对象。
-
内部类可以直接访问外部类的 static 成员,不需要外部类的对象/类名。
-
sizeof(外部类) = 外部类
,和内部类没有任何关系。
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 0) : _i(x) {}
// 内部类
class B
{
public:
B(int y = 0) : _j(y) {}
void func(const A& a)
{
cout << _s << " " << a._i << endl;
}
private:
int _j;
};
private:
static int _s;
int _i;
};
int A::_s = 1;
int main()
{
cout << sizeof(A) << endl; // 4
A a(10);
A::B b;
b.func(a); // 1 10
return 0;
}
五、匿名对象
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 0) : _i(x)
{
cout << "A(int x = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _i;
};
int main()
{
// 非匿名对象:
// A a1;
// A a2(10);
// 匿名对象(顾名思义,即没有名字的对象):
A();
A(10);
return 0;
}
5.1 - 匿名对象的特性
-
匿名对象的生命周期只有它所在的那一行:
-
匿名对象具有常性:
// A& ra = A(10); // error(权限放大) const A& ra = A(10); // ok
-
使用常引用会延长匿名对象的生命周期:
5.2 - 匿名对象的使用场景
-
当方法只调用一次的时候就可以使用匿名对象:
#include <iostream> using namespace std; class Solution { public: int SumSolution(int n) { int sum = 0; for (int i = 1; i <= n; ++i) { sum += i; } return sum; } }; int main() { cout << Solution().SumSolution(100) << endl; // 5050 return 0; }
-
当作参数进行传递:
#include <iostream> using namespace std; class Date { friend void Display(const Date& d); public: Date(int year = 1949, int month = 10, int day = 1) : _year(year), _month(month), _day(day) {} private: int _year; int _month; int _day; }; void Display(const Date& d) { cout << d._year << "-" << d._month << "-" << d._day << endl; } int main() { Display(Date(2023, 5, 1)); // 2023-5-1 return 0; }
六、编译器所做的一些优化
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 0) : _i(x)
{
cout << "A(int x = 0)" << endl;
}
A(const A& a) : _i(a._i)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
if (this != &a)
{
_i = a._i;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _i;
};
void func1(A a)
{
}
A func2()
{
A a;
return a;
}
int main()
{
func1(10);
// 构造(隐式类型转换)+ 拷贝构造 --> 优化成一个构造
cout << "------------------" << endl;
func1(A(10));
// 构造 + 拷贝构造 --> 优化成一个构造
cout << "------------------" << endl;
A ret1 = func2();
// 在一个表达式中,连续的拷贝构造 --> 优化成一个拷贝构造
cout << "------------------" << endl;
// A ret2;
// 1. 构造
// ret2 = func2();
// 2. 赋值运算符重载
// 编译器无法优化
return 0;
}
使用 explicit 关键字修饰构造函数,可以禁止类型转换。