1 类规范
一般来说,类规范由两个部分组成:
- 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
- 类方法定义:描述如何实现类成员函数。
2 内联方法
其定义位于类声明中的函数都将自动称为内联函数,类声明常将短小的成员函数作为内联函数。
内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义,确保内联定义对多文件程序中的所有文件都可用的、最简便的方法是:将内联定义放在定义类的头文件中。
// 内联函数的定义在类声明内
class Test1 {
public:
void SetNum(int num)
{
this->num = num;
}
private:
int num;
};
// 内联函数的定义在类声明外
class Test2 {
public:
void SetNum(int num);
private:
int num;
};
inline void Test2::SetNum(int num)
{
this->num = num;
}
3 构造函数和析构函数
类构造函数用于构造新对象,并将值赋给对象的成员们。
- 在使用构造函数初始化一个对象时,需要注意初始化方式和赋值方式的区别:
class Test3 {
public:
Test3(int num)
{
this->num = num;
}
private:
int num;
};
void test_Test3()
{
// 1、初始化方式1。根据编译器实现,可能会创建临时对象也可能不会
Test3 t1 = Test3(1);
// 2、初始化方式2。不会创建临时对象
Test3 t2(2);
// 3、赋值方式。会先创建一个临时对象,将临时对象复制到t2后,再销毁临时对象
t2 = Test3(3);
}
- 接受一个参数的构造函数允许使用赋值句法来将对象初始化为一个值:
// Classname obj = value;
Test3 t4 = 4;
类析构函数用于释放对象申请的资源,一般是在对象声明周期结束后,由程序自动调用。
4 this指针
this指针设置为调用它的对象的地址,被作为隐藏参数传递给类方法。
5 类对象数组
类对象数组在初始化时,如果没有显式调用构造函数进行初始化时,会自动调用默认构造函数对元素进行初始化。
class Test4 {
public:
Test4(int num)
{
this->num = num;
cout << "test4 num" << endl;
}
Test4()
{
cout << "test4 default" << endl;
}
private:
int num;
};
void test_Test4()
{
Test4 ts1[3] = { // 调用有参构造函数
Test4(1),
Test4(2),
Test4(3),
};
Test4 ts2[3]; // 调用默认构造函数
}
6 操作符重载
C++允许将操作符重载扩展到用户定义的类型,操作符函数的格式如下:
operator op (argument-list)
C++对用户定义的操作符重载的限制:
-
重载后的操作符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载操作符。
-
使用操作符时不能违反操作符原来的句法规则。
-
不能定义新的操作符。
-
不能重载下面的操作符:
- sizeof——sizeof操作符
- .——成员操作符
- .*——成员指针操作符
- ::——作用域解析操作符
- ?:——条件操作符
- typeid——一个RTTI操作符
- const_cast——强制类型转换操作符
- dynamic_cast——强制类型转换操作符
- reinterpret_cast——强制类型转换操作符
- static_cast——强制类型转换操作符
但是,以下表中的操作符都可以被重载:
-
表11.1中的大多数操作符都可以通过成员或非成员函数进行重载,但以下操作符只能通过成员函数重载:
- =——赋值操作符
- ()——函数调用操作符
- []——下标操作符
- ->——通过指针访问类成员的操作符
以加减法操作符为例,其运算需要两个操作数。对于成员函数版本来说,一个操作数通过this指针隐式传递,另一个操作数作为函数参数显式传递;对于友元函数版本来说,两个操作数都作为函数参数传递。
class Time {
public:
// 1、成员函数的操作符重载: T1 + T2 => T1.operator+(T2)
Time operator+(const Time& t)
{
Time tt;
tt.num = t.num + this->num;
return tt;
}
friend Time operator-(const Time& t1, const Time& t2); // 声明为友元函数
private:
int num;
};
// 2、非成员函数的操作符重载,需要声明为友元函数,以便访问类私有成员: T1 - T2 => operator-(T1,T2)
Time operator-(const Time& t1, const Time& t2)
{
Time tt;
tt.num = t1.num - t2.num;
return tt;
}
7 友元
友元可以实现外部对类对象私有成员的访问,友元有3种——友元函数、友元类、友元成员函数。
8 类的自动转换
当类存在单参数的构造函数时,C++允许使用=操作符将该类型参数直接赋值给类。
例如以下程序,将先会调用构造函数Test(int)创建一个临时对象,并将10作为初始化值。随后,采用逐成员赋值方式将该临时对象的内容复制到t对象中,这一过程称为隐式转换。
class Test {
public:
Test(int num) {}
};
Test t = 10;
编译器会在以下场景使用这种隐式转换,有时可能会导致我们的程序产生无意识的错误:
- 将Test对象初始化为int值时。
- 将int值赋给Test对象时。
- 将int值传递给接受Test参数的函数时。
- 返回值被声明为Test的函数试图返回一个int值时。
- 在上述任意一种情况,使用可转换为int类型的内置类型时。
因此,通常在声明单参数的构造函数时,都建议使用explicit
声明构造函数来关闭这种特性:
class Test {
public:
explicit Test(int num) {}
};
Test t = 10; // 编译报错
Test t = (Test)10; // 仍然允许显式转换
Test t = Test(10); // 允许
9 类的强制类型转换
转换函数可以用于类类型到某种类型的转换,其限制及定义格式如下:
- 转换函数必须是类方法。
- 转换函数不能指定返回类型。
- 转换函数不能有参数。
operator typeName();
需要注意的是,当类定义了两种或更多的转换时,使用隐式的类型转换可能会因为产生二义性而被编译器拒绝。
class Test {
public:
operator double() { return 1.0; }
operator int() { return 2; }
};
int main()
{
Test t;
long num1 = t; // 编译报错
long num2 = (int)t; // 允许
long num3 = (double)t; // 允许
return 0;
}
10 隐式成员函数
在类没有主动定义以下成员函数时,C++会自动生成这些函数的定义:
- 默认构造函数。
- 复制构造函数。
- 赋值操作符。
- 默认析构函数。
- 地址操作符。
class Test {};
// 1、如果没有提供任何构造函数,编译器将创建默认构造函数
Test::Test() {}
// 2、每当程序生成了对象副本时,编译器都将使用复制构造函数
Test::Test(const Test& test)
{
// 默认的复制构造函数会逐个复制非静态成员的值
}
// 3、将已有对象赋给另外一个对象时,将使用重载的赋值操作符
Test& Test::operator= (const Test& test)
{
// 与复制构造函数实现类似,也是对成员进行逐个复制
}
11 成员初始化列表
成员初始化列表
由逗号分隔的初始化列表组成(前面带冒号),用于在对象创建时对成员进行初始化(即执行构造函数体内代码前)。
- 这种格式只能用于构造函数。
- 必须用这种格式来初始化非静态const数据成员。
- 必须用这种格式来初始化引用数据成员。
数据成员初始化的顺序与它们在类声明中的顺序相同,与初始化器中的排列顺序无关。
class Test {
public:
// 先初始化num,再初始化str
Test(const string& s) : str(s), num(0) {}
private:
const int num;
string str;
};