首页 > 其他分享 >08.类:更深层次的理解

08.类:更深层次的理解

时间:2024-08-31 13:15:33浏览次数:3  
标签:const 函数 int 08 深层次 理解 对象 成员 构造函数

8. 类:更深层次的理解

8.1 接口与实现分离

我们的每个先前的类定义示例都将一个类放在一个头文件中进行重用,然后将类的定义包含到一个包含main的源代码文件中,这样我们就可以创建和操作类的对象。

传统的思想认为,使用类的一个对象,客户(例如main函数)只需要知道:

1.调用什么成员函数

2.需要向每个成员函数提供什么实参

3.期望从每个成员函数得到什么返回类型

客户端代码不需要知道这些功能是如何实现的。

隐藏类的实现细节使得更容易更改类的实现,同时最小化,并有希望消除对客户端代码的更改。

两个重要的C++软件工程概念:

1.接口与实现分离(Separating interface from implementation.)

2.在头文件中使用include guard以防止头文件中的代码被包含在同一个源代码文件中不止一次。由于一个类只能被定义一次,因此使用这样的预处理指令可以防止多定义错误。

宏保护除了文件名外,最好再加上包名或者自动生成的时间信息,避免异包同名文件的情况

8.1.1 类的接口

接口定义和规范了人和系统等事物之间的交互方式。例如,收音机的控件作为收音机用户与其内部组件之间的接口。控件允许用户执行一组有限的操作(如更改台站、调整音量、在AM台和FM台之间选择等)。不同的收音机可以实现操作的方式不同- -有的提供按钮,有的提供拨号,有的支持语音指令。该接口规定了无线电允许用户执行哪些操作,但没有说明这些操作是如何在无线电内部实现的。

类似地,类的接口描述了类的客户可以使用哪些服务以及如何请求这些服务,而不是类如何执行这些服务。类的Public接口由类的public成员函数(也被称为类的public services)组成。就像你很快看到的那样,你可以通过编写一个类定义来指定一个类的接口,这个类定义只列出类的成员函数原型和类的数据成员。

8.1.2 Separating the Interface from the Implementation(分离类的接口与实现)

为了将类的接口从实现中分离出来,我们将类Time分解成两个文件- -定义类Time的头文件Time . h和定义类Time成员函数的源代码文件Time . cpp ,因此:

1.类是可重用的

2.类的客户知道类提供哪些成员函数,如何调用它们以及期望的返回类型

3.客户端不知道类的成员函数是如何实现的

根据约定,成员函数的定义放在和类同名的源文件中,且文件扩展名为.cpp。即成员函数在一个单独的.cpp文件中定义,类在.h文件中定义,客户即包含main函数的程序在另一个.cpp文件中定义。

8.1.3 ifndef/define/endif

ifndef/define/endif主要目的是防止头文件的重复包含和编译C/C++中#ifndef的用法 - 简书 (jianshu.com) 

#include <string>
#ifndef TEST_10_13_TIME_H
#define TEST_10_13_TIME_H
class Time{
public:
    void setTime(int,int,int);
    std::string toUniversalString() const;
    std::string toStandardString() const;
private:
    unsigned int hour{0};
    unsigned int minute{0};
    unsigned int second{0};
};
#endif //TEST_10_13_TIME_H

当我们构建更大的程序时,其他的定义和声明也会放在头文件中。由于各种原因,头文件可能会出现问题。在项目编译期间,通常会编译每个.cpp文件。简单来说,这意味着编译器将获取您的.cpp文件,打开包含在其中的所有文件,将它们全部连接成一个大文本文件,然后执行语法分析,最后将其转换为一些中间代码,进行优化/执行其他任务,最后生成目标体系结构的程序集输出。因此,如果一个文件被多次包含在一个.cpp文件下,则编译器会将其文件内容添加两次,因此,如果该文件中包含定义,则会出现编译器错误,提示您重新定义了一个变量。当文件在编译过程中由预处理器步骤处理时,第一次到达其内容时,前两行将检查是否已为预处理器定义了CLASS_H。如果没有,它将定义CLASS_H并继续处理它与#endif指令之间的代码。下一次预处理器看到文件的内容时,对FILE_H的检查将为false,因此它将立即向下扫描到#endif并在此之后继续。这样可以防止重新定义错误。

当Time . h被首次#include时,标识符TIME _ H还没有被定义。在这种情况下,#define指令定义了TIME _ H,预处理器包含了. cpp文件中Time . h头文件的内容。如果再#include头文件,则由于已经定义了TIME _ H,预处理器将忽略# ifndef和# endif之间的代码。

使用# ifndef,# defined和# endif预处理指令来形成一个include guard,以防止头文件在源代码文件中被多次包含。

8.1.4 作用域解析运算符(Scope Resolution Operator)

在代码中,在类的名字后面加作用域解析运算符,再加成员函数的名称。此运算符将成员函数与类的定义联系在一起。Time::告诉编译器每个成员函数都在该类的范围内,并且它的名字为其他类成员所知道。

“::”指明了成员函数所属的类。如:M::f(s)就表示f(s)是类M的成员函数。 作用域,如果想在类的外部引用静态成员函数,或在类的外部定义成员函数都要用到。使用命名空间里的类型或函数也要用到(如:std::cout, std::cin, std::string 等等)

没有Time::,编译器就无法知道成员函数属于Time类。相反,编译器会认为它们是"自由(free)的"或"松散(loose)的"函数,比如main - -这些函数也被称为全局函数。这种函数无法访问类的private数据或者调用类的成员函数。

8.1.5 Time Class Member Function toUniversalString and String Stream Processing

cout是标准输出流,ostringstream类的对象(来自头文件<sstream>)提供了相同的功能,但是会将输出写入内存中的string对象。使用ostringstream类的str成员函数得到格式化的string。

string Time::toUniversalString() const {
    ostringstream output;
    output<<setfill('0')<<setw(2)<<hour<<":"<<setw(2)<<minute<<":"<<setw(2)<<second;
    return output.str();
}

在上述代码中,ostringstream类创造了output对象。像使用cout一样使用output即可。参数化流操作符setfill用来指定填充字符,当一个整数在一个比该值的位数更宽的字段中输出时。填充字符出现在数字的左边,因为数字默认右对齐。例如分钟值为2,将会显示为02,因为填充字符设定为'0'。如果数值为两位数,如28,就不会进行填充,一旦操作符setfill使用,后面的所有值都会进行填充(sticky setting)。Ostringstream的str成员函数得到格式化字符串,返回给客户端。

Each sticky setting (such as a fill character or precision) should be restored to its previous setting when it’s no longer needed. Failure to do so may result in incorrectly formatted output later in a program.

8.1.6 Implicitly Inlining Member Functions

如果在类的体中定义了成员函数,则成员函数被隐式地声明为内联的。请记住,编译器保留不内联任何函数的权利。

在类定义内部定义一个成员函数,将成员函数(如果编译器选择这样做)内联。这样可以提高性能。

在类头文件中只定义最简单、最稳定的成员函数(也就是说,其实现方式不可能改变),因为对头的每一次更改都需要重新编译依赖于该头(在大型系统中,一项耗时的工作)的每个源代码文件。

8.1.7 成员函数对比全局函数

使用面向对象编程方法在调用函数时往往需要较少的参数。这种好处源于将数据成员和成员函数封装在一个类中,赋予了成员函数访问数据成员的权利。

成员函数通常比非面向对象程序中的函数更短,因为数据成员中存储的数据在理想情况下已经被构造函数或存储新数据的成员函数验证。由于数据已经在对象中,成员函数调用往往没有参数或者参数少于非面向对象语言中的函数调用。因此,函数调用、函数定义和函数原型都更短。这改善了程序开发的许多方面。

成员函数调用通常比常规函数调用(非面向对象的语言)不带或少带参数,这降低了传递错误参数、错误参数类型或错误参数数量的可能性。

8.1.8 对象的尺寸(Object Size)

刚接触面向对象编程的人常常假设对象必须相当大,因为它们包含数据成员和成员函数。从逻辑上讲,这是真的- -你可以把对象看作是包含数据和函数的(而我们的讨论也肯定鼓励了这一观点);然而,从物理上看,事实并非如此。

对象只包含数据,因此对象比同时包含成员函数时要小得多。编译器从类的所有对象中分别创建成员函数的一个副本(仅)。类中的所有对象共享这一个副本。当然,每个对象都需要自己的类的数据副本,因为数据可以在对象之间变化。函数代码对于类中的所有对象都是一样的,因此,它们之间可以共享。

8.2 编译和链接过程

下图展示了编译和链接过程,生成可供使用的可执行Time应用程序。通常一个类的接口和实现将由一个程序员创建和编译,并由单独的程序员使用,程序员实现使用该类的客户代码。因此,该图显示了类实现程序员和客户代码程序员的需求。图中的虚线分别显示了类实现程序员、客户代码程序员和Time application user所需的片段。

一个类实现程序员负责创建一个可重用的Time类,创建头部Time . h和包含头部的源代码文件Time . cpp,然后编译源代码文件来创建Time的目标代码。为了隐藏类的成员函数实现细节,类实现程序员会向客户代码程序员提供报头Time . h (它规定了类的接口和数据成员)和Time对象代码(即代表Time成员函数的机器码指令)。客户代码程序员没有被赋予Time . cpp,因此客户仍然不知道Time的成员函数是如何实现的。

客户端代码程序员只需要知道Time的接口就可以使用类,并且必须能够链接它的目标代码。由于类的接口是Time . h头中类定义的一部分,因此客户代码程序员必须有权访问该文件,并且必须#将其包含在客户的源代码文件中。在客户端代码编译时,编译器使用Time . h中的类定义,保证主函数正确创建和操作类Time的对象。

要创建可执行的Time应用程序,最后一步就是链接:

  1. the object code for the main function (i.e., the client code),
  2. the object code for class Time’s member-function implementations, and
  3. the C++ Standard Library object code for the C++ classes (e.g., string) used by the class-implementation programmer and the client-code programmer.

8.3 Class Scope and Accessing Class Members(类的范围和访问类的成员)

类的数据成员和成员函数属于类的范围。默认情况下,非成员函数被定义在全局命名范围(global namespace scope)。在类的范围内,类的成员可以被所有的类的成员函数访问并且可以通过名字被引用。在类的范围之外,public类成员可以通过下列三种方式被访问:

1.对象的名字

2.对一个对象的引用

3.一个对象的指针

我们称之为对象上的句柄。句柄的类型使编译器能够确定客户端通过该句柄可访问的接口(例如,成员函数)。

在类的作用域之外,类成员通过对象上的一个句柄(对象名、对对象的引用或对对象的指针)进行引用。

可以通过对象的名字或对象的引用,在后面加.访问对象的成员。通过指向对象的指针引用对象的成员,指针的名字和(->)后跟成员的名字,即:pointerName->memberName。

8.4 Access Functions and Utility Functions(访问函数和辅助函数)

8.4.1 访问函数(access functions)

访问函数可以读取或显示数据,而不修改数据。访问函数的另一个常用的应用是检验条件的真伪---这种函数通常被叫做谓词函数(predicate funcations),例如,任何容器类的empty函数- -一个能够容纳许多对象的类,如数组或向量。程序可能在试图从容器对象中读取另一个项目之前测试empty函数。

8.4.2 辅助函数

辅助函数(helper function)是一个private成员函数,它支持类的其他成员函数的操作。辅助函数被声明为private的,因为他们不是被类的客户使用的。一个常用的辅助函数的用途是在一个函数中放置一些通常的代码,这些代码本来可以在其他几个成员函数中复制。

8.5 构造函数及析构函数

如果某个类的成员函数已经提供了该类的构造函数或其他成员函数所需的全部或部分功能,则从该构造函数或其他成员函数中调用该成员函数。这简化了代码的维护,并降低了如果修改代码实现时出错的可能性。一般来说:避免重复代码。

构造函数可以调用类的其他成员函数,如set或get函数,但由于构造函数是对对象的初始化,数据成员可能还没有被初始化。在数据成员被正确初始化之前使用数据成员会导致逻辑错误。

将数据成员私有化,通过public成员函数控制对这些数据成员的访问,特别是写访问,有助于保证数据的完整性。

数据完整性的好处并不是自动生成的,因为数据成员是private的- -您必须提供适当的有效性检查。

8.5.1 重载构造函数以及委托构造函数

类的构造函数和成员函数也可以重载。重载构造函数允许对象初始化不同类型and/or数量的实参。要重载一个构造函数,应该在类定义中为每个版本的构造函数提供一个原型,并为每个重载版本提供一个单独的构造函数定义。这也适用于类的成员函数。例如,对于下列代码:

Clock(int newH,int newM,int newS):hour(newH),minute(newM),second(newS){ }//构造函数
Clock():hour(0),minute(0),second(0){ }//构造函数

可以使用委托构造函数的形式,将上述代码改写为:

Clock(int newH,int newM,int newS):hour(newH),minute(newM),second(newS){ }
Clock:Clock(0,0,0){ }

就像构造函数可以调用类的成员函数,C++11允许构造函数调用同一类中的其他构造函数。调用的这个构造函数叫做委托构造函数 ,将自己的工作委托给其他构造函数。

8.5.2 委托构造函数/代理构造函数

即一个构造函数可以调用另外的构造函数

class A{
public:
    A():A(0){}//调用了带一个参数的构造函数
    A(int i):A(i,0){}
    A(int i,int j){
        num1=i;
        num2=j;
        average=(num1+num2)/2;
    }
private:
    int num1;
    int num2;
    int average;
};

被委托的构造函数要放在主调构造函数的初始化构造列表位置。

避免递归调用构造函数

8.6 析构函数(Destructors)

析构函数是一种成员函数的类型。析构函数的参数列表是空的。析构函数的命名格式为~加类的名字:

~类名()

析构函数也许不会指定形参以及返回类型。当对象销毁时,析构函数被隐式的调用。例如,当程序执行离开对象被实例化的范围时,对象就会被破坏。析构函数本身并没有释放对象的内存---它执行终止事务管理(termination housekeeping)的功能在对象的内存再生之前,所以内存可能被用于存储新的对象。

每个类都有析构函数,如果没有显式的定义析构函数,编译器会定义一个"empty"析构函数。

析构函数不可以重载,可以手动调用。构造函数不能手动调用。

8.7 当构造函数和析构函数都被调用

这些函数调用发生的顺序取决于执行进入和离开对象实例化的范围的顺序。一般的,析构函数的调用和它对应的构造函数的调用顺序相反。

8.7.1 全局范围内对象的构造函数和析构函数

在其他程序中的任何函数(包括main函数)开始执行之前(尽管全局对象的构造函数的执行顺序并不保证),构造函数被定义在全局范围的对象调用。当main终止时,相应的析构函数被调用。exit函数强迫程序立即终止并且不执行本地对象的析构函数。exit函数通常出现在程序发生不可修复的错误时。abort函数和exit函数类似,会迫使程序立即终止,并且不允许调用程序员定义的任何类型的清理代码。

8.7.2 non-static局部对象的构造函数和析构函数

非静态局部对象的构造函数在执行到达该对象被定义的位置时被调用,相应的析构函数在执行离开对象的范围(也就是说,该对象所定义的块已经执行完毕)时被调用。

如果程序以exit函数或abort函数的方式终止,则不调用本地对象的析构函数。

8.7.3 static局部对象的构造函数和析构函数

静态局部对象的构造函数只调用一次,当执行首先到达对象被定义的地方时- -当main程序终止或程序调用exit函数退出时,相应的析构函数被调用。

全局和static对象销毁的顺序和他们创建的顺序相反。static对象不会调用析构函数如果程序调用abort函数终止。

8.7.4 构造函数和析构函数何时被调用

//
// Created by 22364 on 2023/10/15.
//
#include <string>

#ifndef TEAT_10_15_2_CREATEANDDESTROY_H
#define TEAT_10_15_2_CREATEANDDESTROY_H

class CreateAndDestroy{
public:
    CreateAndDestroy(int,std::string);
    ~CreateAndDestroy();

private:
    int objectID;
    std::string message;
};
#endif //TEAT_10_15_2_CREATEANDDESTROY_H
#include <iostream>
#include "CreateAndDestroy.h"
using namespace std;

CreateAndDestroy::CreateAndDestroy(int ID,string messageString)
    :objectID{ID},message{messageString} {
    cout<<"object "<<objectID<<" constructor runs "
    <<message<<endl;
}

CreateAndDestroy::~CreateAndDestroy(){
    cout<<(objectID==1||objectID==6?"\n":"");
    cout<<"object "<<objectID<<" destroy runs "
    <<message<<endl;
}
#include <iostream>
#include "CreateAndDestroy.h"
using namespace std;

void create();
CreateAndDestroy first{1,"(gobal before main)"};//全局对象

int main() {
    cout<<"\\nMAIN FUNCTION: EXECUTION BEGINS"<<endl;
    CreateAndDestroy second{2,"(local in main)"};
    static CreateAndDestroy third{3,"(local static in main)"};

    create();

    cout<<"\\nMAIN FUNCTION: EXECUTION RESUMES"<<endl;
    CreateAndDestroy fourth{4,"(local in main)"};
    cout<<"\\nMAIN FUNCTION: EXECUTION ENDS"<<endl;
}

void create() {
    cout << "\nCREATE FUNCTION: EXECUTION BEGINS" << endl;
    CreateAndDestroy fifth{5, "(local in create)"};
    static CreateAndDestroy sixth{6, "(local static in create)"};
    CreateAndDestroy seventh{7, "(local in create)"};
    cout << "\nCREATE FUNCTION: EXECUTION ENDS" << endl;
}

上述代码在main函数中声明了三个对象,当程序执行到对象声明处时,每个对象的构造函数被调用。fourth和second对象的析构函数调用的顺序和他们构造函数的调用顺序相反,当程序的执行到达main函数的结尾时。因为third对象声明为static的,所以在程序终止以后仍然存在。third对象的析构函数的调用在first对象的析构函数调用之前,但是在所有其他对象销毁之后。

seventh的析构函数先调用再调用fifth对象的析构函数(和他们构造函数的调用顺序相反),在create函数终止时。sixth对象被声明为static,在程序终止以后仍然存在。sixth对象的析构函数调用在third和first对象的析构函数的调用之前,但是在所有其他对象销毁之后。

8.8 Default Memberwise Assignment(默认逐成员初始化)

//
// Created by 22364 on 2023/10/17.
#include <string>

#ifndef TEST_10_17_DATE_H
#define TEST_10_17_DATE_H

class Date{
public:
    explicit Date(unsigned int=1,unsigned int =1,unsigned int=2000);
    std::string toString() const;
private:
    unsigned int month;
    unsigned int day;
    unsigned int year;
};

#endif //TEST_10_17_DATE_H
#include <sstream>
#include <string>
#include "Date.h"
using namespace std;

Date::Date(unsigned int m, unsigned int d, unsigned int y)
    :month{m},day{d},year{y} {}
string Date::toString() const {
    ostringstream output;
    output<<month<<'/'<<day<<'/'<<year;
    return output.str();
}
#include <iostream>
#include "Date.h"
using namespace std;

int main() {
    Date date1{7,4,2004};
    Date date2;

    cout<<"date1= "<<date1.toString()
        <<"\ndate2="<<date2.toString()<<"\n\n";

    date2=date1;

    cout<<"after default memberwise assignment,date2="
    <<date2.toString()<<endl;

}

赋值操作符=可以用来将同一个类的一个对象赋值给另一个对象。默认情况下,默认情况下,这种指派是通过memberwise assignment(也称为copy assignment)进行的- -赋值运算符右侧对象的每个数据成员被单独赋值到赋值运算符左侧对象中的同一个数据成员。

注意: 当使用的类的数据成员包含指向动态地址的指针时, Memberwise assignment会引起严重的问题;我们在后续内容讨论了这些问题,并给出了如何处理这些问题的方法。

对象可以作为函数参数传递,也可以从函数返回。这种传递和返回默认情况下使用数值传递执行- -传递或返回对象的一个副本。在这种情况下,C + +创建一个新的对象,并使用一个拷贝构造函数将原对象的值拷贝到新对象中。对于每个类,编译器提供一个默认的副本构造器,将原对象的每个成员复制到新对象的相应成员中。同成员分配一样,拷贝构造函数在与数据成员包含指针的类一起使用动态分配内存时会引起严重的问题。

8.9 实例化(instantiation)

就像有人在实际驾驶汽车之前必须从工程图纸中创建一个汽车一样,在一个程序能够执行类的成员函数定义的任务之前,必须从类中创建一个对象。这样做的过程被称为实例化。然后,一个对象被称为它类的一个实例。

8.10 const对象和const成员函数

一些对象不需要被改变,就可以声明为const。You may use const to specify that an object is not modifiable and that any attempt to modify the object should result in a compilation error.

const Time noon{12,0,0};

C++不允许const对象调用成员函数,除非成员函数也被声明为const的。即使对于不修改对象的get成员函数也是如此。这也是我们将所有不修改被调用对象的成员函数都声明为const的一个关键原因。

const对象只能调用const成员函数

const成员函数只能调用const成员函数

const成员函数中不能改变成员变量的值

定义一个const成员函数调用non-const成员函数是编译错误。

const对象调用non-const对象是编译错误

试图将构造函数和析构函数声明为const是编译错误。

即使成员函数没有没有改变对象,但是const对象仍然不能调用它,成员函数必须显式的声明为const,才可以被const对象调用。

8.11 Composition: Objects as Members of Classes(组成:对象作为类的成员)

//Date.h
// Created by 22364 on 2023/10/17.
#include <string>

#ifndef TEST_10_17_DATE_H
#define TEST_10_17_DATE_H

class Date{
public:
    static const unsigned int monthsPerYear{12};
    explicit Date(unsigned int=1,unsigned int =1,unsigned int=1900);
    std::string toString() const;
    ~Date();// provided to confirm destruction order
private:
    unsigned int month;
    unsigned int day;
    unsigned int year;

    unsigned int checkDay(int) const;
};

#endif //TEST_10_17_DATE_H
//Date.cpp
#include <sstream>
#include "Date.h"
#include <array>
#include <iostream>
#include <stdexcept>
using namespace std;

Date::Date(unsigned int mn, unsigned int dy, unsigned int yr)
    :month{mn},day{dy},year{yr} {
    if(mn<1||mn>monthsPerYear){
        throw invalid_argument("month must be 0-12");
    }
    cout<<"date object constructor for date"<<toString()<<endl;
}
string Date::toString() const {
    ostringstream output;
    output<<month<<'/'<<day<<'/'<<year;
    return output.str();
}

Date::~Date(){
    cout<<"date object destructor for date"<<toString()<<endl;
}

unsigned int Date::checkDay(int testDay) const {
    static const array<int, monthsPerYear + 1> daysPerMonth{
        0,31,28,31,30,31,30,31,31,30,31,30,31};
    if(testDay>0&&testDay<=daysPerMonth[month]){
        return testDay;
    }
    if (month==2&&testDay==29&&(year%400==0||(year%4==0&&year%100!=0))){
        return testDay;
    }
    throw invalid_argument("Invalid day for current month and year");
}
//Employ.h
#include <string>

#ifndef TEST_10_17_EMPLOYEE_H
#define TEST_10_17_EMPLOYEE_H

#include <string>
#include "Date.h"

class Employee{
public:
    Employee(const std::string&,const std::string&,const Date&,const Date&);
    std::string toString() const;
    ~Employee();

private:
    std::string firstName;
    std::string lastName;
    const Date birthDate;
    const Date hireDate;
};
#endif //TEST_10_17_EMPLOYEE_H
//Employ.cpp
#include <iostream>
#include <sstream>
#include "Employee.h"
#include "Date.h"
using namespace std;

Employee::Employee(const std::string& first, const std::string& last, const Date &dateOfBirth, const Date &dateOfHire)
    :firstName(first),lastName(last), birthDate(dateOfBirth), hireDate(dateOfHire){
    cout<<"Employee object constructor: "<<firstName<<' '<<lastName<<endl;
}

string Employee::toString() const {
    ostringstream output;
    output<<lastName<<", "<<firstName<<" Hired: "<<hireDate.toString()<<" birthday: "<<birthDate.toString();
    return output.str();
}

Employee::~Employee(){
    cout<<"Employee object destructor: "<<lastName << ", " << firstName << endl;
}
//main.cpp
#include <iostream>
#include "Date.h"
#include "Employee.h"
using namespace std;


int main() {
    Date birth{7,24,1949};
    Date hire{3,12,1988};
    Employee manager{"Bob","Blue",birth,hire};

    cout<<"\n"<<manager.toString()<<endl;
}

在一个类中包含其他类的对象,就叫做composition(组合)(or aggregation(聚合))。现在,我们展示了一个类的构造函数如何通过成员初始化子(member initializers)将参数传递给成员对象构造函数。

数据成员按照在类定义(不是按照它们在构造函数member-initializer list的顺序)中声明的顺序进行构造,并在构造其包含的类对象之前进行构造。

8.11.1 类的默认副本构造函数

在类class的定义内,构造函数没有接收Date类型的形参,那么,Employee构造函数的成员初始化列表为什么可以通过向Date类的构造函数传递Date类的对象来初始化birthDate和hireDate。正如我们在之前提到的,编译器为每个类提供了一个默认的复制构造函数,它将构造函数的参数对象的每个数据成员复制到被初始化对象的相应成员中。

即,birth和hire均是Date的对象,Employee的对象manager将对象birth和hire作为实参传递给Employee的构造函数,在Employee的构造函数的成员初始化列表里,将Date对象dateOfBirth和dateOfHire传递给在Employee类中定义的private数据成员,此数据成员即为const的Date对象birthDate和hireDate。问题在于,Date类的构造函数并没有接收对象的形参,原因就在于类的默认副本构造函数。

上诉运行的结果,可以看出析构函数执行的顺序。这些输出证实了Employee对象是从外部被销毁的,即Employee析构函数先运行(输出显示从输出窗口底部开始的五条线),然后成员对象被破坏的顺序与它们被构造的顺序相反。

string类的析构函数不包含输出语句。

8.11.2 如果不使用成员初始化列表?

如果一个成员对象不通过成员初始化列表初始化,成员对象的默认构造函数会被隐式调用。默认构造函数建立的值(如果有的话)可以用set函数重写。然而,对于复杂的初始化,这种方法可能需要大量的额外工作和时间。

如果一个数据成员是另一个类的对象,public该成员对象并不违反对该成员对象private成员的封装和隐藏。但是,它却违背了封装类实现的封装性和隐藏性,所以类的成员对象仍然应该是private的。

8.12 友元函数和友元类(friend Functions and friend Classes)

类的友元函数是一个非成员函数,有权访问类的public和non-public成员。 单独的函数,整个类或者其他类的成员函数都可以被声明为其他类的friends。友元的目的就是让一个函数或者类访问另一个类中的private成员

在当前类以外定义的、不属于当前类的函数也可以在类中声明,但要在前面加 friend 关键字,这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。

友元函数可以访问当前类中的所有成员,包括 public、protected、private 属性的。

还可以将整个类声明为另一个类的“friend”,这就是友元类。友元类中的所有成员函数都是另外一个类的友元函数。类的友元关系是单向的。例如:如果声明B类是A类的友元,B类的成员函数就可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有保护数据。

8.12.1 友元的声明

将classTwo声明为classOne的友类,只要在classOne的定义中声明如下语句:

friend class ClassTwo;

friend的声明可以出现在类的任意位置,不受访问限定符public和private和protected的影响。友谊是被给予的,而不是被接受的——为了让B类成为a类的朋友,a类必须明确声明B类是它的朋友。友谊不是对称的,如果A类是B类的朋友,你就不能推断B类是A类的朋友。友谊是不可传递的——如果A类是B类的朋友,B类是C类的朋友——你就不能推断出A类是C类的朋友。

8.12.2 通过友元函数修改类的private成员

#include <iostream>
using namespace std;

class Count{
   friend void setX(Count&,int);
public:
    int getX() const{return x;}
private:
    int x{0};
};

void setX(Count& c,int val){
    c.x=val;
}

int main(){
    Count counter;
    cout<<"counter.x after instantiation: " << counter.getX() << endl;
    setX(counter,8);//25 set x using a friend function 友元函数直接在main函数中调用
    cout<<"counter.x after call to setX friend function: "
    <<counter.getX()<<endl;
}

setX函数是一个独立(全局)函数,不是Count类的成员函数。由于此原因,当setX函数被counter对象调用,19行的语句将counter作为实参传给setX,而不是使用对象的名字调用函数---例如(counter.setX(8))。函数setX被允许访问类Count的私有数据成员x,只是因为setX被声明为该类的friend。

可以将重载函数指定为类的友元。每个想要成为友元的函数都必须在类定义中明确声明为类的友元。

即使友元函数的原型出现在类定义中,友元也不是成员函数。

将所有friend声明首先放在类定义的主体内,并且不要在它们之前使用任何访问说明符。

友元类和友元函数打破了类的封装性

8.13 使用this指针

每个类的功能只有一个副本,但一个类可以有很多对象,那么成员函数如何知道要操作哪个对象的数据成员呢?每个对象都可以通过一个名为this(C++关键字)的指针访问自己的地址。this指针不是对象本身的一部分,也就是说,this指针占用的内存不会反映在对象的sizeof操作的结果中。相反,this指针(由编译器)作为隐式参数传递给对象的每个非静态成员函数。后续介绍了静态类成员,并解释了为什么this指针没有隐式传递给静态成员函数。

this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。

所谓当前对象,是指正在使用的对象。例如对于​stu.show();​ ,stu 就是当前对象,this 就指向 stu。this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性的。**

this 虽然用在类的内部,但是只有在对象被创建以后才会给 this 赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给 this 赋值。

8.13.1 使用this指针避免命名冲突

void Time::setHour(int hour){
    if(hour>=0&&hour<24){
        this->hour=hour;
    }
    else{
        throw invalid_argument("hour must be 0-23");
    }
}

一种通常的显式的使用this指针的方法是防止类的数据成员和成员函数形参(或者是其他局部变量)的命名冲突

例如,一个成员函数的局部变量和类的数据成员名字相同,如上述代码所示,局部变量被认为隐藏hide或遮盖shadow了数据成员---仅使用函数体内的局部变量的名字代表局部变量而不是数据成员。但是可以使用->操作符访问数据成员。

this->hour=hour;

8.13.2 this指针的类型

this指针的类型取决于对象的类型,以及在其中使用this指针的成员函数是否声明为const:

1.在Employee类的non-const成员函数中,this指针的类型为Employee* const——一个指向nonconstant Employee的constant指针。

2.在const成员函数,this指针的类型为const Employee* const——指向constant Employee的constant指针。

8.13.3 隐式和显式的使用this指针访问对象的数据成员

#include <iostream>
using namespace std;

class Test{
public:
    explicit Test(int);
    void print() const;
private:
    int x{0};
};

Test::Test(int value)
    :x{value} {
}

void Test::print() const {
    cout << " x = " << x;
    cout << "\n this->x = " << this->x;
    cout << "\n(*this).x = " << (*this).x << endl;
}

int main() {
    Test testObject{12};
    testObject.print();
}

this指针的一个有趣的用途是防止对象被分配给它自己

8.13.4 使用this指针完成级联函数(Cascaded Function)调用

this指针的另一个用途是启用级联成员函数调用——也就是说,在同一语句中顺序调用多个函数。

//
// Created by 22364 on 2023/10/20.
//
#include <string>

#ifndef TEST_10_20_TIME_H
#define TEST_10_20_TIME_H

class Time{
public:
    explicit Time(int=0,int=0,int=0);

    Time& setTime(int,int,int);
    Time& setHour(int);
    Time& setMinute(int);
    Time& setSecond(int);

    unsigned int getHour() const;
    unsigned int getMinute() const;
    unsigned int getSecond() const;
    std::string toUniversalString() const;
    std::string toStandardString() const;
private:
    unsigned int hour{0};
    unsigned int minute{0};
    unsigned int second{0};

};
#endif //TEST_10_20_TIME_H
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include "Time.h"
using namespace std;

Time::Time(int hr, int min, int sec) {
    setTime(hr, min, sec);
}

Time& Time::setTime(int h, int m, int s){
    setHour(h);
    setMinute(m);
    setSecond(s);
    return *this;
}

Time& Time::setHour(int h){
    if (h >= 0 && h < 24){
        hour = h;
    }
    else{
        throw invalid_argument("hour must be 0-23");
    }
    return *this;
}

Time& Time::setMinute(int m){
    if (m >= 0 && m < 60){
        minute = m;
    }
    else{
        throw invalid_argument("minute must be 0-59");
    }
    return *this;
}
Time& Time::setSecond(int s){
    if (s >= 0 && s < 60){
        second = s;
    }
    else{
        throw invalid_argument("second must be 0-59");
    }
    return *this;
}

unsigned int Time::getHour() const {return hour;}

unsigned int Time::getMinute() const {return minute;}

unsigned int Time::getSecond() const {return second;}

string Time::toUniversalString() const{
    ostringstream output;
    output << setfill('0') << setw(2) << getHour() << ":"
    <<setw(2) << getMinute() << ":" << setw(2) << getSecond();
    return output.str();
}

string Time::toStandardString() const{
    ostringstream output;
    output << ((getHour() == 0 || getHour() == 12) ? 12 : getHour() % 12)
    <<":" << setfill('0') << setw(2) << getMinute() << ":" << setw(2)
    <<getSecond() << (hour < 12 ? " AM" : " PM");
    return output.str();
}
#include <iostream>
#include "Time.h"
using namespace std;

int main(){
    Time t;
    t.setHour(18).setMinute(30).setSecond(22);// cascaded function calls

    cout<<"Universal time: " << t.toUniversalString()
    <<"\nStandard time: " << t.toStandardString();

    cout<<"\n\nNew standard time: "
    <<t.setTime(20, 20, 20).toStandardString()<<endl;//cascading
}

8.14 static类成员(静态成员)

这个规则有一个重要的例外,即类的每个对象都有自己的类的所有数据成员的副本。在某些情况下,类的所有对象只应共享变量的一个副本。使用静态数据成员是出于这些和其他原因。这样的变量表示“类范围”的信息,即由所有实例共享的数据,并且不特定于类的任何一个对象。即,静态数据成员为该类的所有对象共享,静态数据成员具有静态生存期。

8.14.1 Motivating Classwide Data

当一个类的所有对象都有一个数据副本就足够了时,使用静态数据成员来节省存储空间,比如一个可以由该类所有对象共享的常量。

8.14.2 static数据成员的范围和初始化

基本类型静态数据成员默认初始化为0。 如果静态数据成员是提供默认构造函数的类的对象,则无需初始化该静态数据成员,因为将调用其默认构造函数。静态成员变量只能放在所有成员函数的外部进行初始化。

1.声明为“constexpr”类型的静态数据成员必须在类中声明并初始化。

2.声明为“inline”(C++17起)或者“const int”类型的静态数据成员可以在类中声明并初始化。

3.其他情况下,静态数据成员必须在类的外部进行定义并初始化,且不带static关键字。

8.14.3 访问静态数据成员

通常使用类的public的成员函数或者fridends访问类的private(以及protected)静态成员。类的静态成员存在,即使不存在该类的对象。要在不存在类的对象时访问public静态类成员,只需在数据成员的名称前面加上类名和作用域解析运算符(::)。例如,如果我们前面的变量martianCount是公共的,则可以使用表达式Martian::martianCount来访问它,即使在没有火星对象的情况下也是如此。 (当然,不鼓励使用公共数据。)

要在不存在私有或受保护的静态类成员的对象时访问该类成员,请提供一个公共静态成员函数,并通过在其名称前加类名和作用域解析运算符来调用该函数。静态成员函数是类的服务,而不是类的特定对象的服务。

类的静态数据成员和静态成员函数存在,即使没有实例化该类的对象,也可以使用。

例如以下代码:

class A{
public:
    A(int a=0){
        x=a;
    }
    static void f1();
    static void f2(A a);
private:
    int x;
    static int y;
};
void A::f2(A a){
    cout<<A::y;
	cout<<x;//error 静态成员函数访问非静态成员变量,只能通过对象的名字访问
	cout<<a.x;//right
}
void A::f1(){
	cout<<A::y<<endl;
}
int main(){
	A::f1();
	A mA(3);
	A::f2(mA);
	mA.A::f1();
}

当变量和函数不依赖于类的实例时,在类中使用静态成员。

8.14.4 Demonstrating static Data Members

#ifndef TEST_10_20_2_EMPLOYEE_H
#define TEST_10_20_2_EMPLOYEE_H
#include <string>

class Employee{
public:
    Employee(const std::string&,const std::string&);
    ~Employee();
    std::string getFirstName() const;
    std::string getLastName() const;

    static unsigned int getCount();
private:
    std::string firstName;
    std::string lastName;

    static unsigned int count;
};
#endif //TEST_10_20_2_EMPLOYEE_H
#include <iostream>
#include "Employee.h"
using namespace std;

unsigned int Employee::count{0};

unsigned int Employee::getCount() {return count;}

Employee::Employee(const std::string& first, const std::string& last)
    :firstName(first),lastName(last){
    ++count;
    cout << "Employee constructor for " << firstName
    <<' ' << lastName << " called." << endl;
}

Employee::~Employee(){
    cout<<"~Employee() called for " << firstName
    <<' ' << lastName << endl;
    --count;
}

string Employee::getFirstName() const {return firstName;}

string Employee::getLastName() const {return lastName;}
#include <iostream>
#include "Employee.h"
using namespace std;

int main() {
    cout<<"Number of employees before instantiation of any objects is "
    <<Employee::getCount()<<endl;
    {
        Employee e1{"Susan","Baker"};
        Employee e2{"Robert","Jones"};

        cout<<"Number of employees after objects are instantiated is "
        <<Employee::getCount();

        cout<<"\n\nEmployee 1: "
        <<e1.getFirstName() << " " << e1.getLastName()
        <<"\nEmployee 2: "
        <<e2.getFirstName() << " " << e2.getLastName() << "\n\n";
    }

    cout << "\nNumber of employees after objects are deleted is "
    <<Employee::getCount()<<endl;
}

通过上图可以看到,在Employee的对象创造之前,就使用了getCount函数,在Employee对象销毁后,仍然可以使用getCount函数。如果成员函数不访问类的非静态数据成员或非静态成员函数,则应将其声明为静态。与非静态成员函数不同,静态成员函数没有this指针,因为静态数据成员和静态成员函数独立于类的任何对象而存在。this指针必须引用类的特定对象,并且当调用静态成员函数时,内存中可能没有该类的任何对象。

Declaring a static member function const is a compilation error. The const qualifier indicates that a function cannot modify the contents of the object on which it operates, but static member functions exist and operate independently of any objects of the class.

8.14.4.1 nested scope

If two different entities named by the same identifier are in scope at the same time, and they belong to the same name space, the scopes are nested (no other form of scope overlap is allowed), and the declaration that appears in the inner scope hides the declaration that appears in the outer scope:

// The name space here is ordinary identifiers.
 
int a;   // file scope of name a begins here
 
void f(void)
{
    int a = 1; // the block scope of the name a begins here; hides file-scope a
    {
      int a = 2;         // the scope of the inner a begins here, outer a is hidden
      printf("%d\n", a); // inner a is in scope, prints 2
    }                    // the block scope of the inner a ends here
    printf("%d\n", a);   // the outer a is in scope, prints 1
}                        // the scope of the outer a ends here
 
void g(int a);   // name a has function prototype scope; hides file-scope a

8.14.5 命名空间

8.14.5.1 概述

在c++中,名称(name)可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大,名称互相冲突性的可能性越大。另外使用多个厂商的类库时,也可能导致名称冲突。为了避免在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,标准C++引入关键字namespace(命名空间/名字空间/名称空间),可以更好地控制标识符的作用域。

8.14.5.2 定义
//定义一个名字为A的命名空间(变量、函数)
namespace A {
    int a = 100;
}
namespace B {
    int a = 200;
}
void test02()
{
    //A::a  a是属于A中
    cout<<"A中a = "<<A::a<<endl;//100
    cout<<"B中a = "<<B::a<<endl;//200
}
8.14.5.3 命名空间只能全局范围内定义(以下为错误写法)

8.14.5.4 命名空间可以嵌套
namespace A {
    int a = 1000;
    namespace B {
        int a = 2000;
    }
}
void test03()
{
    cout<<"A中的a = "<<A::a<<endl; //1000
    cout<<"B中的a = "<<A::B::a<<endl; //2000
}
8.14.5.5 命名空间是开放的,即可以随时把新的成员加入已有的命名空间中(常用)
namespace A {
    int a = 100;
    int b = 200;
}
//将c添加到已有的命名空间A中
namespace A {
    int c = 300;
}
void test04()
{
    cout<<"A中a = "<<A::a<<endl;//100
    cout<<"A中c = "<<A::c<<endl;//200
}
8.14.5.6 命名空间 可以存放 变量 和 函数
namespace A {
    int a=100;//变量
 
    void func()//函数
    {
        cout<<"func遍历a = "<<a<<endl;
    }
}
void test05()
{
    //变量的使用
    cout<<"A中的a = "<<A::a<<endl;
 
    //函数的使用
    A::func();
}
8.14.5.7 命名空间中的函数 可以在“命名空间”外 定义
namespace A {
    int a=100;//变量
 
    void func();
}
 
void A::func()//成员函数 在外部定义的时候 记得加作用域
{
    //访问命名空间的数据不用加作用域
    cout<<"func遍历a = "<<a<<endl;
}
 
void funb()//普通函数
{
    cout<<"funb遍历a = "<<A::a<<endl;
}
void test06()
{
   A::func();
    funb();
}

无名命名空间,意味着命名空间中的标识符只能在本文件内访问,相当于给这个标识符加上了static,使得其可以作为内部连接(了解)

namespace{
    int a = 10;
    void func(){
        cout<<"hello namespace"<<endl;
    }
}
void test(){
 
    //只能在当前源文件直接访问a 或 func
    cout<<"a = "<<a<<endl;
    func();
}

8.15 对象数组

声明方式1:

Circle ca1[10];

声明方式2:用匿名对象构成的列表初始化数组

auto ca2[3]={Circle{3},Circle{},Circle{5}};

声明方式3:使用C++11列表初始化,列表成员为隐式构造的匿名对象

Circle ca3[3]{3.1,{},5};
Circle ca4[3]={3.1,{},5};

声明方式4:用new在堆区生成对象数组

auto* p1=new Circle[3];
auto p2=new Circle[3]{3.1,{},5};
delete []p1;
delete []p2;
p1=p2=nullptr;

标签:const,函数,int,08,深层次,理解,对象,成员,构造函数
From: https://www.cnblogs.com/yyyylllll/p/18390160

相关文章

  • 3.从对变量的理解到数据类型的一种解释
    1变量1.1变量在使用过程中有三点注意事项1.变量必须是字母数字和下划线组成2.变量的命名开头不能是数字,数字会与python中的数字重复,导致错误3.变量不能使用内置的字,如print1.2变量的规范1.一般情况下需要进行分割,就是用_2.变量在使用的过程中也要注意,不要乱命名,会导致后续......
  • 抽象,解耦,粒度,关系的个人理解
    学习java和k8s时发现他们的概念有共性,觉得比较有意思记录一下.只是由技术引发的乱七八糟的想法,和具体技术没有太大关系,写着玩(#^.^#)延展思考后,提炼出四个关键词,先概述下,后面分别详述:1.抽象:代码解决了很多重复性的工作,能够实现去重的基础就是从千奇百怪的具象事......
  • 2024/08/31 每日一题
    LeetCode3127构造相同颜色的正方形方法1:模拟classSolution{publicbooleancanMakeSquare(char[][]grid){for(inti=0;i<2;i++){for(intj=0;j<2;j++){intcnt=0;//统计黑色数量cnt+=......
  • 2024/08/29 每日一题
    LeetCode3142判断矩阵是否满足条件方法1:模拟classSolution{publicbooleansatisfiesConditions(int[][]grid){intn=grid.length,m=grid[0].length;for(inti=0;i<n;i++){for(intj=0;j<m;j++){......
  • 2024/08/30 每日一题
    LeetCode3153所有数对中数位差之和方法1:模拟classSolution{publiclongsumDigitDifferences(int[]nums){intn=nums.length;longans=0;while(nums[0]>0){//遍历每一位inttol=0;//统计总个数i......
  • Leetcode 第 408 场周赛题解
    Leetcode第408场周赛题解Leetcode第408场周赛题解题目1:3232.判断是否可以赢得数字游戏思路代码复杂度分析题目2:3233.统计不是特殊数字的数字数量思路代码复杂度分析题目3:3234.统计1显著的字符串的数量思路代码复杂度分析题目4:3235.判断矩形的两个角落是否......
  • 工作随意总结20240830
    最近被裁换工作了,面试过了一个测开岗位,正好趁着入职前的空闲时间,总结一下过去工作中的经历,想到哪写到哪吧,定期总结很重要。工作主要在一家ToB云服务厂商从事测试工作,工作内容主要涵盖了功能、自动化、渗透、性能等方方面面,覆盖到面很广,单一某方面来说深度上就有一些欠缺......
  • VBA语言専攻简介0831
    VBA语言専攻简介0831在当今世界,几乎没有任何工作是没有计算机的。有些工作需要定期重复相同的过程,最好将它们自动化。一旦任务自动化,只需单击一个按钮即可运行。VBA是实现自动化工作的最为简单的方式,它不需要其他工具,因为它已经与MicrosoftOffice软件集成。VBA是VisualBasicfor......
  • AI学会“视听”新语言,人大北邮上海AI Lab引领多模态理解革命 | ECCV2024亮点
    你是否想过,AI是如何“理解”我们这个多彩世界的呢?最近,一项由中国人民大学高瓴GeWu-Lab、北京邮电大学、上海AILab等机构联合研究的成果,为AI的“感官”升级提供了一种新思路。这项研究被收录于即将召开的计算机视觉顶级会议ECCV2024。AI的“视听盛宴”想象一下,你正在观......
  • 08 - debugfs
    ----整理自王利涛老师课程实验环境:宅学部落www.zhaixue.cc文章目录0.什么是debugfs1.debugfs配置编译和注册运行2.第一个debugfs编程示例3.通过debugfs导出整型数据4.通过debugfs导出16进制数据5.通过debugfs导出数组6.通过debugfs导出内存......