【C++Primer笔记】第七章 类

本文深入探讨了C++中的类设计原则,包括封装的优势、构造函数的使用、拷贝和赋值操作、访问控制与封装、友元、类型定义、内联函数、常量成员以及静态成员等关键概念。同时,介绍了如何通过类的特性来实现抽象数据类型,以及如何合理设计构造函数、拷贝行为和析构函数,确保对象状态的正确管理。此外,还讨论了字面值常量类和静态成员的特殊性质。
摘要由CSDN通过智能技术生成

优秀的类设计者应该密切关注使用该类的程序员(用户)的需求。

封装的益处:

  • 确保用户代码不会无意间破坏封装对象的状态
  • 被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码

每个类定义了唯一的类型。即使两个类的成员完全一样,它们也是不同的类型。

定义抽象数据类型

  1. 定义在类内部的函数是隐式的内联函数。成员函数声明必须在类内,定义可在类外(必须包含它所属的类名)或类内。作为接口组成部分的非成员函数,定义和声明都在类的外部(其声明与类放在同一个头文件内)。

  2. 常量对象、常量对象的引用或指针只能调用常量成员函数。

    std::string isbn() const {return bookNo};
    

    this指针默认情况下是一个指向非常量版本的常量指针,所以默认时(非常量成员函数)不能把this指针绑定到常量对象上。而当我们把const放到函数形参列表后的时候,this就成为一个指向常量的常量指针;也因此,常量函数不能改变它的对象的内容。

  3. 编译器首先编译成员的声明,然后才轮到函数体。因此,成员函数可以随意使用类中的其他成员而无需在意次序。

  4. 一般来说,当我们定义的函数类似于某个内置运算符时,应该令该函数行为尽量模仿这个函数运算符(如:内置的赋值运算符把它的左侧运算对象当成左值返回)。

  5. 因为IO对象属于不能被拷贝的类型,所以我们只能通过引用来传递它们:

    ostream &print(ostream &os, const Sales_data &item)
    {...}
    

构造函数

注意:构造函数不能被声明为const的。

当我们创建一个类的const对象时,直到构造函数完成初始化的过程,对象才能真正取得其“常量”属性。因此,构造函数在const对象的构造过程中可以向其写值。

  1. 合成的默认构造函数:如果存在类内的初始值(用={}表示),则用它来初始化成员;否则默认初始化该成员。

  2. 只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。

  3. 如果类内包含有内置类型或复合类型的成员,则只有当这些成员都被赋予了类内初始值时,这个类才适合使用合成的默认构造函数。

  4. 在C++新标准中,如果我们需要默认的行为,可以在参数列表后面加上= default来要求编译器生成构造函数。

    Sales_data() = default;
    

    如果编译器不支持类内初始值,该默认构造函数就应该使用函数初始值列表来初始化。

  5. 没有出现在函数初始值列表中的成员将通过相应的类内初始值初始化,或者执行默认初始化。

  6. 构造函数不应该轻易覆盖掉类内初始值,除非新赋的值和原值不同。如果你不能使用类内初始值,则所有的构造函数都应该显示地初始化每个内置类型成员。

拷贝、赋值和析构

使用vector或string的类能避免分配和释放内存带来的复杂性。

  1. 一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁工作。

访问控制与封装

使用class 和struct定义类的唯一区别就是默认的访问权限。

  1. public成员定义类的接口(构造函数和部分成员函数);private隐藏了类的实现细节(数据成员和作为实现部分的函数)。
  2. 如果使用struct关键字,则定义在第一个访问说明符之前的成员时public的;如果使用class关键字,则是private的。出于统一编程风格的考虑,当我们希望定义的类的所有成员是public时,使用struct,反之使用class

友元

友元仅仅指定了访问权限,而非一个通常意义上的函数声明!最好(对有些编译器而言)在友元声明之外(类的外部)在对函数进行一次声明。

注:即使在类的内部定义友元函数,也要在类的外部提供声明。

类可以允许其他类或函数访问其非公有成员,方法是令其成为友元。

class Sales_data {
friend Sales_data add(const Sales_data&, const Sales_data&);
...
};
Sales_data add(const Sales_data&, const Sales_data&);

一般来说,最好在类定义或结束前的位置集中声明友元。

  1. 如果一个类指定了友元类,其友元类的成员函数可以访问该类的非公有成员。友元关系不具有传递性。

  2. 把一个成员函数声明为友元时,必须明确指出该成员函数属于哪个类。且该函数必须被提前声明!严格按照如下顺序:

    • 定义类A,声明func函数,但不定义
    • 定义类B,包括对func的友元声明friend A::func();
    • 定义func,此时它才可使用B的成员

    注:重载函数需要一个一个地声明为友元

  3. 不过,类和非成员函数的声明倒不是必须在它们的友元声明之前。

类的其他特性

  1. 定义一个类型成员

    class Screen {
    public:
    	typedef std::string::size_type pos;
    private:
    	pos curser = 0;
    	...
    };
    

    通过把pos定义成public成员,可以隐藏Screen实现的细节。用来定义类型成员的成员必须先定义后使用。

  2. 在类中,常有一些规模小的函数被声明为内联函数。定义在类内的函数自动inline,定义在类外部的函数,可以在定义处加上inline,使其成为内联函数。

  3. 可变数据成员:在变量声明时加入mutable关键字。无论对象是否为常量,该成员都是可变的。

  4. 类数据成员的初始值:在C++新标准中,最好的方式就是把这个默认值声明为一个类内初始值:

    class Window_mgr {
    private:
    	std::vector<Screen> screens{Screen{24,80,' '}};
    };
    

返回*this的成员函数

  1. 返回左值,可以连续运算:

    class Screen {
    public:
    	Screen &set(char);
    };
    inline Screen &Screen::set(char c);
    //move函数也是返回Screen&
    myScreen.move(4,0).set('#');
    
  2. const成员函数的this指针指向一个const对象,因此,返回类型应该是常量引用。于是,该成员函数就不能嵌入到一组改变动作的序列中去。这是合理的:一个常量对象并不能被做出任何变动。

  3. 建议:对于公共代码使用私有功能函数

    class Screen {
    public:
    	Screen &display(std::ostream &os);
    				   {do_display; return *this;}
    	const Screen &display const (std::ostream &os);
    	               {do_display; return *this;}
    private:
    	void do_display(std::ostream &os) //内联函数
    				   {os << contents;}
    }
    

    设计良好的代码常常包含大量类似于do_dispkay的小函数。通过调用这些函数,可以完成其他一组函数的实际工作。

  4. class Sales_date s;
    //Sales_date s; 和上一句等级(struct同理)
    

    第一种方式是从C语言继承而来。

  5. 前向声明class Screen;前向声明后、定义前,该类型为不完全类型。只能定义指向这种类型的指针或引用,也可以声明(不能定义)以不完全类型为参数或返回类型的函数。其余情况,直到类被定义之后,数据成员才能被声明为这一类型。

类的作用域

  1. 在类的外部,成员的名字被隐藏了。

    Screen::pos ht = 24, wd = 80;
    //使用Screen定义的pos类型
    

    在类外定义的函数,如果想要使用类中定义的类型作为返回类型,必须指定是哪个类定义了它

  2. 成员函数体中能使用类中定义的任何名字;而成员函数返回类型或参数列表中使用的名字,必须在使用前可见。成员函数体中名字查找顺序:

    • 在成员函数内查找
    • 在类内继续查找
    • 在成员函数定义之前的作用域内找

    不建议使用其他成员的名字作为某个函数的参数!(尽管类的成员被隐藏了,但我们仍可以通过::this->来强制访问)

  3. 一般来说,内层作用域可以重新定义外层作用于的名字。但是,如果成员使用了外层定义域中的某个名字,则该名字代表一种类型,不能在类内重新定义。

  4. 类型名的定义通常放在类的开始处,以确保所有使用该类型的成员都能出现在该类型的定义之后。

构造函数再探

在实际中,如果定义了其他构造函数,最好也提供一个默认构造函数。

  1. 如果没有在构造函数初始值列表中显式地初始化成员,该成员将在构造函数体之前执行默认初始化。**如果成员是const、引用或某种没有默认构造函数的类,必须将其初始化(这是唯一的机会)。**事实上,列表初始化也比在函数体中初始化效率高(前者直接初始化,后者先初始化再赋值)。因此,养成使用初始化列表的习惯

  2. 初始值列表中,成员的初始化顺序只和它们的出现顺序有关(先出现先初始化)。最好令构造函数初始值的顺序和成员声明的顺序一致,并且尽量避免使用某些成员初始化其他成员。

  3. 如果一个构造函数为所有的参数都提供了默认实参,则它实际上也定义了默认构造函数。

  4. 委托构造函数

    class Sales_data {
    public:
    	Sales_data(std::string s, unsigned cnt):
    		bookNo(s), units_sold(cnt) {}
    	//下面三个都是委托构造函数
    	Sales_data(): Sales_data(" ", 0, 0) {}
    	Sales_data(std::string s): Sales_data(s, 0, 0) {}
        Sales_data(std::istream &is): Sale_data()
        { read(is, *this); }
    }
    

    如果被委托的函数体包含代码,委托者将先执行这些代码,然后控制权才会交还给委托者的函数体。

转换构造函数

  1. 转换构造函数:如果构造函数只接收一个实参,那么它实际上定义了转换为此类型的隐式转换机制 —— 在需要该类的的地方,可以用实参类型的对象替代。但是,编译器只会自动地执行一步转换

    string null_book = "9-999-99999-9"; //一次转换
    item.combine(null_book); //正确
    item.combine("9-999-99999-9"); //两次转换,错误!
    //显示转换,正确
    item.combine(string("9-999-99999-9")); 
    item.combine(Sales_data("9-999-99999-9"));
    
  2. 如果要抑制构造函数的隐式转换,我们可以在构造函数前加上explicit关键字。该关键字只对一个参数的构造函数有效,且**必须在类内声明构造函数时使用,在类外定义时不能重复。**(和staticfriend一样)

  3. 由于用=执行拷贝形式的初始化时会发生隐式转换,所以对于explicit构造函数,我们只能使用直接初始化

    Sales_data item1(null_book); //正确
    Sales_data item2 = null_book; //错误!
    //但我们还是可以使用这样的构造函数显式地强制进行转换
    item.combine(Sales_data(null_book));
    //static_cast使用istream构造函数创建了一个Sales_data临时对象
    item.combine(static_cast<Sales_data>(cin));
    

聚合类

  1. 聚合类使得用户可以直接访问其成员。满足以下条件的被称为“聚合的”:

    • 所有成员都是public
    • 没有定义任何构造函数
    • 没有类内初始值
    • 没有基类,也没有virtual函数
  2. 我们可以提供一个花括号来初始化聚合类的数据成员。初始值的顺序必须和声明顺序一致:

    struct Data {
        int ival;
        string s;
    }
    Data vall = {0, "Anna"};
    
  3. 缺点:将正确初始化每个成员的重任交给了用户;添加或删除一个成员后,所有的初始化语句都需要更新。

字面值常量类

  • 数据成员都是字面值类型的聚合类字面值常量类
  • 数据成员都是字面值类型、至少含有一个constexpr构造函数、类内初始值是常量表达式(或者使用类成员自己的constexpr构造函数初始化)、使用析构函数的默认定义的类是字面值常量类

constexpr构造函数体一般是空的,且必须初始化所有数据成员。

class Debug {
public:
	constexpr Debug(bool b=true):hw(b),io(b),other(b){}
    constexpr Debug(bool h, bool i, bool o):
    				hw(b),io(b),other(b){}
    constexpr any(){return hw||io||other;}
    void set_hw(bool b){hw = b;}
    void set_io(bool b){io = b;}
    void set_other(bool b){other = b;}
private:
    bool hw; //硬件错误
    bool io; //IO错误
    bool other; //其他错误
};
//constexpr构造函数用于生成constexpr对象以及constexpr函数的参数和返回类型
constexpr Debug io_sub(false, true, false);
if (io_sub.any())
    cerr << "Debug IO" << endl;

类的静态成员

静态成员存在于程序的整个生命周期之中。

  1. 静态成员函数不与任何对象绑定在一起,不包含this指针,因此静态成员函数不能被声明为const,且不能在static函数中使用this指针。

  2. 可以使用作用域运算符直接访问静态成员,也可以使用类的对象、引用或指针来访问。

  3. explicit一样,不能重复static关键字。该关键字只出现在类内部的声明语句。且一般来说,我们不在类的内部初始化静态成员,而是**在类的外部定义初始化(一般放在类的源文件中)。**

    double Account::interestRate = initRate();
    
  4. 如果静态成员是字面值常量类型(constexpr),可以在类内用const整型类型初始化(即便这时候也要在类外定义一下)。

  5. 静态数据成员可以是不完全类型(特别的,就是它所属的类类型)。也可以使用静态数据成员作为默认实参。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值