ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

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_passengerNameUnknown Passenger;m_numberOfMiles0;m_hasEliteSuperRewardsStatusfalse;}然而如果构造函数只是初始化数据成员而不做其他事情实际上就没有必要使用构造函数因为可在类定义中直接初始化数据成员也称为类内初始化。例如不编写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_passengerNamename;}intAirlineTicket::getNumberOfMiles(){returnm_numberOfMiles;}voidAirlineTicket::setNumberOfMiles(intmiles){m_numberOfMilesmiles;}boolAirlineTicket::hasEliteSuperRewardsStatus(){returnm_hasEliteSuperRewardsStatus;}voidAirlineTicket::setHasEliteSuperRewardsStatus(boolstatus){m_hasEliteSuperRewardsStatusstatus;}也可以直接将方法实现直接放在模块接口文件中。示例如下exportclassAirlineTicket{public:doublecalculatePriceInDollars(){if(hasEliteSuperRewardsStatus()){return0;}returngetNumberOfMiles()*0.1;};std::stringgetPassengerName(){returnm_passengerName;}voidsetPassengerName(std::string name){m_passengerNamename;}intgetNumberOfMiles(){returnm_numberOfMiles;}voidsetNumberOfMiles(intmiles){m_numberOfMilesmiles;}boolhasEliteSuperRewardsStatus(){returnm_hasEliteSuperRewardsStatus;}voidsetHasEliteSuperRewardsStatus(boolstatus){m_hasEliteSuperRewardsStatusstatus;}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::coutstd::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::coutd.get()\n;// prints 5std::coutNS::get()\n;// prints 20std::cout::get()\n;// prints 10std::coutget()\n;// prints 10}请注意如果将名为NS的名称空间定义为未命名的/匿名的则以下代码将导致有歧义的名称解析的编译错误因为你会有一个定义在全局作用域中的get()以及一个定义在未命名的名称空间中的get()。std::cout get() \n;如果你在main函数之前使用了如下的using命令也会发生同样的错误。using namespace NS;3. 统一初始化(高度建议)在C11之前各类型的初始化并非总是统一的。例如考虑以下两个定义其中一个作为结构体另一个作为类。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;};在C11之前CircleStruct类型变量和CircleClass类型变量的初始化是不同的。CircleStruct myCircle1{10,10,2.5};CircleClassmyCircle2(10,10,2.5);对于结构体版本可使用{…}语法。然而对于类版本需要使用函数符号(…)调用构造函数。自C11以后允许一律使用{…}语法初始化类型如下所示。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.firstInitalJ;anEmployee.lastInitalD;anEmployee.employeeNumber42;anEmployee.salary80000;使用统一初始化可以写成这样Employee anEmployee { J, D, 42, 80000 };使用统一初始化并不局限于结构和类它还可用于初始化C中的任何内容。例如下面的代码把所有4个变量都初始化为3。inta3;intb(3);intc{3};// uniform initializationintd{3};// uniform initialization统一初始化还可用对变量进行零初始化只需要指定一对空的大括号。例如int e {}; // uniform initialization, e will be 0.使用统一初始化的一个优点是可以阻止窄化。当使用旧式风格的赋值语法初始化变量时C隐式地执行窄化。例如voidfunc(inti){/* ... */}intmain(){intx3.14;func(3.14);}在main()的两行代码中C在对x赋值或调用func()之前会自动将3.14截断为3。注意有些编译器可能会针对窄化给出警告信息而另一些编译器则不会。在任何情况下窄化转换都不应被忽视因为它们可能会引起细微的错误。使用同一初始化如果编译器完全支持C11标准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 };从C20开始可以省略数组的大小4像下面这样int* myArray new int[] { 0, 1, 2, 3 };统一初始化还可在构造函数初始化器中初始化类成员数组classMyClass{public:MyClass():m_array{0,1,2,3}{}private:std::arrayint,4m_array;};统一初始化还可用于标准库容器。4. 指派初始化C20引入了指派初始化器以使用它们的名称初始化所谓聚合的数据成员。聚合类型是满足以下限制的数组类型的对象或结构或类的对象仅public数据成员、无用户声明或继承的构造函数、无虚函数和无虚基类、private和protected的基类。指派初始化器以.开头后跟数据成员的名称。指派初始化的顺序必须与数据成员的声明顺序相同。不允许混合使用指派初始化器和非指派初始化器。未使用指派初始化器初始化的任何数据成员都将使用其默认值进行初始化这意味着1. 拥有类内初始化器的数据成员会得到该值。2. 没有类内初始化器的数据成员会被零初始化。下面修改了Employee结构以演示指派初始化。structEmployee{charfirstInitial;charlastInitial;intemployeeNumber;intsalary{75000};};在本节前面这种Employee结构是使用如下的统一初始化语法初始化的Employee anEmployee { J, D, 42, 80000 };使用指派初始化器可以写成这样Employee anEmployee{.firstInitialJ,.lastInitialD,.employNumber42,.salary80000};使用指派初始化器的好处是与统一初始化语法相比它更容易理解指派初始化器正在初始化的内容。使用指派初始化器如果对某些成员的默认值感到满意则可以跳过它们的初始化。例如在创建员工时可以跳过初始化employeeNumber在这种情况下employeeNumber初始化为零因为它没有类内初始化器。Employee anEmployee{.firstInitialJ,.lastInitialD,.salary80000};如果使用统一初始化语法这是不可以的必须像下面这样指定employeeNumber为0Employee anEmployee { J, D, 0, 80000 };如果你像下面这样跳过了初始化salary数据成员它就会得到它的默认值即它的类内初始化值75000。Employee anEmployee{.firstInitialJ,.lastInitialD};使用指派初始化器的最后一个好处是当新成员被增加到数据结构时使用指派初始化器的现有代码将继续起作用。新的数据成员将使用其默认值进行初始化。参考[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C20高级编程(第五版)
返回列表