news 2026/9/15 21:47:59

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++和标准库速成(七)——类、作用域解析、统一初始化和指派初始化

目录

  • 1. 类
    • 1.1 定义类
    • 1.2 使用类
  • 2. 作用域解析
  • 3. 统一初始化(高度建议)
  • 4. 指派初始化
  • 参考

1. 类

1.1 定义类

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

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

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

exportmoduleairline_ticket;// import <string>;importstd.core;// MSVC vs18之后需要改为import std;exportclassAirlineTicket{public:AirlineTicket();// constructor~AirlineTicket();// destructordoublecalculatePriceInDollars();std::stringgetPassengerName();voidsetPassengerName(std::string name);intgetNumberOfMiles();voidsetNumberOfMiles(intmiles);boolhasEliteSuperRewardsStatus();voidsetHasEliteSuperRewardsStatus(boolstatus);private:std::string m_passengerName;intm_numberOfMiles;boolm_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"};intm_numberOfMiles{0};boolm_hasEliteSuperRewardsStatus{false};

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

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

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

doubleAirlineTicket::calculatePriceInDollars(){if(hasEliteSuperRewardsStatus()){// elite super rewards customers fly for free!return0;}// the cost of the ticket is the number of milies times 0.1.// real airlines probably have a more complicated formula!returngetNumberOfMiles()*0.1;}std::stringAirlineTicket::getPassengerName(){returnm_passengerName;}voidAirlineTicket::setPassengerName(std::string name){m_passengerName=name;}intAirlineTicket::getNumberOfMiles(){returnm_numberOfMiles;}voidAirlineTicket::setNumberOfMiles(intmiles){m_numberOfMiles=miles;}boolAirlineTicket::hasEliteSuperRewardsStatus(){returnm_hasEliteSuperRewardsStatus;}voidAirlineTicket::setHasEliteSuperRewardsStatus(boolstatus){m_hasEliteSuperRewardsStatus=status;}

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

exportclassAirlineTicket{public:doublecalculatePriceInDollars(){if(hasEliteSuperRewardsStatus()){return0;}returngetNumberOfMiles()*0.1;};std::stringgetPassengerName(){returnm_passengerName;}voidsetPassengerName(std::string name){m_passengerName=name;}intgetNumberOfMiles(){returnm_numberOfMiles;}voidsetNumberOfMiles(intmiles){m_numberOfMiles=miles;}boolhasEliteSuperRewardsStatus(){returnm_hasEliteSuperRewardsStatus;}voidsetHasEliteSuperRewardsStatus(boolstatus){m_hasEliteSuperRewardsStatus=status;}private:boolm_hasEliteSuperRewardsStatus{false};intm_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);doublecost{myTicket.calculatePriceInDollars()};std::cout<<std::format("This ticket will cost ${}\n",cost);

2. 作用域解析

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

classDemo{public:intget(){return5;}};intget(){return10;}namespaceNS{intget(){return20;}}

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

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

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

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

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

structCircleStruct{intx,y;doubleradius;};classCircleClass{public:CircleClass(intx,inty,doubleradius):m_x{x},m_y{y},m_radius{radius}{}private:intm_x,m_y;doublem_radius;};

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

CircleStruct myCircle1={10,10,2.5};CircleClassmyCircle2(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。

inta=3;intb(3);intc={3};// uniform initializationintd{3};// uniform initialization

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

voidfunc(inti){/* ... */}intmain(){intx=3.14;func(3.14);}

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

intx{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 };
统一初始化还可在构造函数初始化器中初始化类成员数组:

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

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

4. 指派初始化

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

structEmployee{charfirstInitial;charlastInitial;intemployeeNumber;intsalary{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高级编程(第五版)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 21:47:20

MoneyPrinterTurbo:免费AI短视频一键生成指南

MoneyPrinterTurbo&#xff1a;免费AI短视频一键生成指南 【免费下载链接】MoneyPrinterTurbo 利用 AI 大模型和自动化工作流&#xff0c;根据主题或关键词一键生成高清短视频。Generate HD short videos from a topic or keyword with an automated AI workflow. 项目地址: …

作者头像 李华
网站建设 2026/9/15 21:46:50

扫码模块二次开发:USB/TTL/RS232接口本质与实战避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 21:45:17

Flutter双端开发全流程:从环境搭建到上架审核的实战经验

三年前我接了个活&#xff0c;客户的需求写得很朴素&#xff1a;iOS 和 Android 各出一版&#xff0c;功能一样&#xff0c;视觉一样&#xff0c;一个月交付。我当时拍着胸脯说没问题&#xff0c;Flutter 双端开发嘛&#xff0c;一套代码的事。真上手才发现&#xff0c;"一…

作者头像 李华
网站建设 2026/9/15 21:43:20

基于YOLOv12的手语识别系统开发全流程解析

1. 项目概述这个基于YOLOv12的手语识别检测系统&#xff0c;是我最近完成的一个很有意思的计算机视觉项目。它能够实时识别视频流中的手语动作&#xff0c;并将其转换为文字或语音输出。作为一个完整的端到端解决方案&#xff0c;它包含了从模型训练到应用部署的全流程实现。我…

作者头像 李华