C++和标准库速成(七)——类、作用域解析、统一初始化和指派初始化

1. 类

1.1 定义类

  类定义了对象的特征。在C++中,类通常在模块接口文件中定义和被导出,然而类的方法定义既可以在相同的模块接口文件中,也可以在对应的模块实现文件中。
  下面定义了一个基本的机票类,这个类可根据飞行的里程数以及顾客是不是“精英超级奖励计划”的成员计算票价。这个定义首先声明了一个类名,在大括号内声明了类的数据成员以及方法。每个数据成员以及方法都具有特定的访问级别:public、protected或private。这些标记可按任意顺序出现,也可重复使用。

访问级别访问范围
public可在类的外部访问
private不能在类的外部访问(推荐将所有的数据成员都声明为private,在需要时可通过获取器和设置器访问它们)
protected之后在讲继承的时候详细讲解

  当写一个模块接口文件时,不要忘记使用export module声明表明你正在写哪个模块,同时也不要忘记将那些你希望对模块使用者可用的类型显式导出

export module airline_ticket;

// import <string>;
import std.core; // MSVC

export class AirlineTicket {
public:
	AirlineTicket(); // constructor
	~AirlineTicket(); // destructor

	double calculatePriceInDollars();

	std::string getPassengerName();
	void setPassengerName(std::string name);

	int getNumberOfMiles();
	void setNumberOfMiles(int miles);

	bool hasEliteSuperRewardsStatus();
	void setHasEliteSuperRewardsStatus(bool status);

private:
	std::string m_passengerName;
	int m_numberOfMiles;
	bool m_hasEliteSuperRewardsStatus;
};

  与类同名但没有返回类型的方法是构造函数,当创建类的对象时会自动调用构造函数。~之后紧跟类名的方法是析构函数,当销毁对象时会自动调用。
  模型接口文件定义了类,然而本例中方法的实现在模块实现文件.cpp中。源文件以如下的模块声明开头,告诉编译器这是airline_ticket模板的源文件。
  module airline_ticket;
  可通过几种方法初始化数据成员。一种方法是使用构造函数初始化器,即在构造函数名称之后加上冒号。示例如下:

AirlineTicket::AirlineTicket() :
	m_passengerName { "Unknown Passenger" },
	m_numberOfMiles { 0 },
	m_hasEliteSuperRewardsStatus { false } {
}

  第二种方法是将初始化任务放在构造函数体中,示例如下:

AirlineTicket::AirlineTicket() {
	// initialize data members.
	m_passengerName = "Unknown Passenger";
	m_numberOfMiles = 0;
	m_hasEliteSuperRewardsStatus = false;
}

  然而,如果构造函数只是初始化数据成员,而不做其他事情,实际上就没有必要使用构造函数,因为可在类定义中直接初始化数据成员,也称为类内初始化。例如,不编写AirlineTicket构造函数,而是直接修改类定义中数据成员的定义,示例如下:

private:
	std::string m_passengerName { "Unknown Passenger" };
	int m_numberOfMiles { 0 };
	bool m_hasEliteSuperRewardsStatus { false };

  如果类还需要执行其他一些初始化类型,如打开文件,分配内存等,则需要编写构造函数进行处理。
  下面是AirlineTicket类的析构函数:

AirlineTicket::~AirlineTicket() {
	// nothong to do in terms of cleanup.
}

  这个析构函数什么都不做,因此可从类中删除。如果需要执行一些清理,如关闭文件、释放内存等,则需要使用析构函数。
  AirlineTicket类方法的定义如下所示:

double AirlineTicket::calculatePriceInDollars() {
	if (hasEliteSuperRewardsStatus()) {
		// elite super rewards customers fly for free!
		return 0;
	}
	// the cost of the ticket is  the number of milies times 0.1.
	// real airlines probably have a more complicated formula!
	return getNumberOfMiles() * 0.1;
}

std::string AirlineTicket::getPassengerName() {
	return m_passengerName;
}

void AirlineTicket::setPassengerName(std::string name) {
	m_passengerName = name;
}

int AirlineTicket::getNumberOfMiles() {
	return m_numberOfMiles;
}

void AirlineTicket::setNumberOfMiles(int miles) {
	m_numberOfMiles = miles;
}

bool AirlineTicket::hasEliteSuperRewardsStatus() {
	return m_hasEliteSuperRewardsStatus;
}

void AirlineTicket::setHasEliteSuperRewardsStatus(bool status) {
	m_hasEliteSuperRewardsStatus = status;
}

  也可以直接将方法实现直接放在模块接口文件中。示例如下:

export class AirlineTicket {
public:
	double calculatePriceInDollars() {
		if (hasEliteSuperRewardsStatus()) {
			return 0;
		}
		return getNumberOfMiles() * 0.1;
	};

	std::string getPassengerName() {
		return m_passengerName;
	}
	void setPassengerName(std::string name) {
		m_passengerName = name;
	}

	int getNumberOfMiles() {
		return m_numberOfMiles;
	}
	void setNumberOfMiles(int miles) {
		m_numberOfMiles = miles;
	}

	bool hasEliteSuperRewardsStatus() {
		return m_hasEliteSuperRewardsStatus;
	}
	void setHasEliteSuperRewardsStatus(bool status) {
		m_hasEliteSuperRewardsStatus = status;
	}

private:
	bool m_hasEliteSuperRewardsStatus { false };
	int m_numberOfMiles{ 0 };
	std::string m_passengerName{ "Unknown Passenger" };
};

1.2 使用类

  为了使用AirlineTicket类,首先需要导入它的模块:
  import airline_ticket;
  下面展示了AirlineTicket类的使用。

AirlineTicket myTicket;
myTicket.setPassengerName("Sherman T. Socketwrench");
myTicket.setNumberOfMiles(700);
double cost { myTicket.calculatePriceInDollars() };
std::cout << std::format("This ticket will cost ${}\n", cost);

2. 作用域解析

  作为C++程序员,需要熟悉作用域的概念。程序中的每个名称都在某个作用域中。可以使用名称空间、函数定义、用花括号分隔的块和类定义来创建作用域。当你尝试访问一个变量、函数或类时,首先在最近的封闭作用域内查找名称,然后在下一个作用域内查找,以此类推,直到全局作用域。不在名称空间、函数、用花括号分隔的块或类中的任何名称都被视为在全局作用域中。如果在全局作用域内未找到,则编译器将提示未定义的符号错误。
  有时,作用域中的名称会覆盖其他作用域中相同的名称。有时,你所需的作用域不是程序中某特定行的默认作用域。如果你不希望名称使用默认作用域解析,则可以使用作用域解析运算符::限定特定作用域的名称。下面的示例展示了这一点,该示例定义了一个Demo类,类中有一个get()方法,还定义了一个全局作用域下的get()函数,以及一个NS名称空间里的get()函数。

class Demo {
public:
	int get() {
		return 5;
	}
};

int get() {
	return 10;
}

namespace NS {
	int get() {
		return 20;
	}
}

  全局作用域是未命名的,但你可以单独使用作用域解析运算符来专门访问它。可以按以下方式调用不同的get()函数。在此例中,代码本身位于main()函数中,该函数始终位于全局范围内。

int main() {
	Demo d;
	std::cout << d.get() << "\n"; // prints 5
	std::cout << NS::get() << "\n"; // prints 20
	std::cout << ::get() << "\n"; // prints 10
	std::cout << get() << "\n"; // prints 10
}

  请注意,如果将名为NS的名称空间定义为未命名的/匿名的,则以下代码将导致有歧义的名称解析的编译错误,因为你会有一个定义在全局作用域中的get(),以及一个定义在未命名的名称空间中的get()。
  std::cout << get() << "\n";
  如果你在main函数之前使用了如下的using命令,也会发生同样的错误。
  using namespace NS;

3. 统一初始化(高度建议)

  在C++11之前,各类型的初始化并非总是统一的。例如,考虑以下两个定义,其中一个作为结构体,另一个作为类。

struct CircleStruct {
	int x, y;
	double radius;
};

class CircleClass {
public:
	CircleClass(int x, int y, double radius) :
		m_x { x }, m_y { y }, m_radius { radius } {
	}
private:
	int m_x, m_y;
	double m_radius;
};

  在C++11之前,CircleStruct类型变量和CircleClass类型变量的初始化是不同的。

CircleStruct myCircle1 = { 10, 10, 2.5 };
CircleClass myCircle2(10, 10, 2.5);

  对于结构体版本,可使用{…}语法。然而对于类版本,需要使用函数符号(…)调用构造函数。自C++11以后,允许一律使用{…}语法初始化类型,如下所示。

CircleStruct myCircle3 = { 10, 10, 2.5 };
CircleClass myCircle4 = { 10, 10, 2.5 };

  定义myCircle4时将自动调用CircleClass的构造函数。甚至等号也是可选的,因此下面的代码与前面的代码等价:

CircleStruct myCircle5 { 10, 10, 2.5 };
CircleClass myCircle6 { 10, 10, 2.5 };

  在结构体一节中出现的另一个例子中,一个Employee结构用如下方法初始化。

Employee anEmployee;
anEmployee.firstInital = 'J';
anEmployee.lastInital = 'D';
anEmployee.employeeNumber = 42;
anEmployee.salary = 80'000;

  使用统一初始化,可以写成这样:
  Employee anEmployee { 'J', 'D', 42, 80'000 };
  使用统一初始化并不局限于结构和类,它还可用于初始化C++中的任何内容。例如,下面的代码把所有4个变量都初始化为3。

int a = 3;
int b(3);
int c = { 3 }; // uniform initialization
int d { 3 }; // uniform initialization

  统一初始化还可用对变量进行零初始化,只需要指定一对空的大括号。例如:
  int e {}; // uniform initialization, e will be 0.
  使用统一初始化的一个优点是可以阻止窄化。当使用旧式风格的赋值语法初始化变量时,C++隐式地执行窄化。例如:

void func(int i) {
	/* ... */
}

int main() {
	int x = 3.14;
	func(3.14);
}

  在main()的两行代码中,C++在对x赋值或调用func()之前,会自动将3.14截断为3。注意有些编译器可能会针对窄化给出警告信息,而另一些编译器则不会。在任何情况下,窄化转换都不应被忽视,因为它们可能会引起细微的错误。使用同一初始化,如果编译器完全支持C++11标准,x的赋值和func()的调用都会生成编译错误。

int x { 3.14 }; // error because narrowing.
func({ 3.14 }); // error because narrowing.

  如果你需要窄化,建议使用准则支持库GSL中提供的gsl::narrow_cast()函数。
  统一初始化还可用来初始化动态分配的数组:
  int* myArray = new int [4] { 0, 1, 2, 3 };
  从C++20开始,可以省略数组的大小4,像下面这样:
  int* myArray = new int[] { 0, 1, 2, 3 };
  统一初始化还可在构造函数初始化器中初始化类成员数组:

class MyClass {
public:
	MyClass() : 
		m_array { 0, 1, 2, 3 } {
	}
private:
	std::array<int, 4> m_array;
};

  统一初始化还可用于标准库容器。

4. 指派初始化

  C++20引入了指派初始化器,以使用它们的名称初始化所谓聚合的数据成员。聚合类型是满足以下限制的数组类型的对象或结构或类的对象:仅public数据成员、无用户声明或继承的构造函数、无虚函数和无虚基类、private和protected的基类。指派初始化器以.开头,后跟数据成员的名称。指派初始化的顺序必须与数据成员的声明顺序相同。不允许混合使用指派初始化器和非指派初始化器。未使用指派初始化器初始化的任何数据成员都将使用其默认值进行初始化,这意味着:
  1. 拥有类内初始化器的数据成员会得到该值。
  2. 没有类内初始化器的数据成员会被零初始化。
  下面修改了Employee结构以演示指派初始化。

struct Employee {
	char firstInitial;
	char lastInitial;
	int employeeNumber;
	int salary { 75'000 };
};

  在本节前面,这种Employee结构是使用如下的统一初始化语法初始化的:
  Employee anEmployee { 'J', 'D', 42, 80'000 };
  使用指派初始化器,可以写成这样:

Employee anEmployee {
	.firstInitial = 'J',
	.lastInitial = 'D',
	.employNumber = 42,
	.salary = 80'000
};

  使用指派初始化器的好处是,与统一初始化语法相比,它更容易理解指派初始化器正在初始化的内容。使用指派初始化器,如果对某些成员的默认值感到满意,则可以跳过它们的初始化。例如,在创建员工时,可以跳过初始化employeeNumber,在这种情况下,employeeNumber初始化为零,因为它没有类内初始化器。

Employee anEmployee {
	.firstInitial = 'J',
	.lastInitial = 'D',
	.salary = 80'000
};

  如果使用统一初始化语法,这是不可以的,必须像下面这样指定employeeNumber为0:
  Employee anEmployee { 'J', 'D', 0, 80'000 };'
  如果你像下面这样跳过了初始化salary数据成员,它就会得到它的默认值,即它的类内初始化值75000。

Employee anEmployee {
	.firstInitial = 'J',
	.lastInitial = 'D'
};

  使用指派初始化器的最后一个好处是,当新成员被增加到数据结构时,使用指派初始化器的现有代码将继续起作用。新的数据成员将使用其默认值进行初始化。

参考

[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C++20高级编程(第五版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值