重读C++Primer学习笔记 类篇

类的基本思想是数据抽象data abstraction封装 encapsulation
数据抽象是一种依赖于接口 interface实现 implementation分离的编程和设计技术
封装实现了类的接口和实现的分离

c++ Primer 第五版 第230页

成员函数的声明必须在类的内部,他的定义既可以在类的内部也可以在外部。作为接口组成部分的非成员函数,定义和声明都在类的外部

struct Sales_data{
  std::string isbn() const {return bookNo;}
  Sales_data& combine(const Sales_data&);
  double avg_price() const;
  std::string bookNo;
  unsigned units_sold =0;
  double revenue = 0.0;
  
};
Sales_data add(const Sales_data&,, const Sales_data&);
std::ostream &print(std::ostream&,, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);

我们再观察一次对isbn成员函数的调用
total.isbn()
当我们调用成员函数时,实际上是在替某个对象调用它。如果isbn指向Sales_data的成员(例如bookNo),它隐式的指向调用该函数对象成员。在上面的调用中,isbn隐式的返回total.bookNo
成员函数通过this的额外隐式参数来访问调用他的对象,也就是说编译器负责将total的地址传递给isbn的隐式形参this

//伪代码 说明调用成员函数的实际执行过程
Sales_data::isbn(&total);

在成员函数内部 我们可以直接调用该函数的对象成员
,而无需通过成员访问运算符做到这一点,因为this所指的正是这个对象。也就是说isbn使用bookNo时,就像我们书写了this->bookNo一样
isbn的另一个关键之处是紧随参数列表后面的const,他的作用是隐式修改this指针的类型
默认情况下,this的类型是指向类类型非常量版本的常量指针
在成员函数中,this的类型是Sales_data * const,隐式参数仍然要遵循初始化规则,意味着(在默认情况下),我们不能把this绑定在 一个常量对象上(因为底层是一个非const),这种情况使得我们不能在一个常量对象上调用普通的成员函数
如果isbn是一个普通函数且this是一个普通指针参数u,那么可以声明成const Sales_dataconst,但由于this是隐式的,所以如何做出类似的声明就成了一个问题了。
C++的做法是允许把const放在成员函数的参数列表之后,
紧随在参数列表之后的const代表this是一个指向常量的指针。这样使用const的成员函数称为常量成员函数*
可以将其想象成

//伪代码 说明其是如何使用的
//非法的,不能显示定义this
std::string Sales_data::isbn(const Sales_data * const this)
{return this->isbn;}

常量对象以及常量对象的引用或指针执泥调用常量对象函数

c++ Primer第五版 第234页

类的作者常常定义一些辅助函数,虽然操作从概念上属于类的接口的组成部分,当并不将其定义为类的成员函数
一般来说,如果非成员函数是类接口的组成部分,这些类的声明应该与类放在一个头文件里面

istream &read(istream &is,Sales_data &item)
{
        double price = 0;
        is >> item.bookNo >> item.units_sold >> price;
        item.revenue = price * item.units;
        return is;
}
ostream &print(ostream &os, const Sales_data &item)
{
        os<< item.isbn()....;
        return os;
}

注意

  • 由于IO类不能被拷贝,所以只能通过引用来传递他们,
  • print不负责换行,尽量减少对格式的控制,可以确保由用户代码来决定是否换行

c++ Primer 第五版 第235页 构造函数

构造函数的名字和类名相同,和其他函数不同,构造函数没有返回类型
构造函数不能被声明为const的
编译器创造的构造函数被称为合成的默认构造函数
对大多数类来说,它的操作是

  • 存在类内初始值的,用于初始化成员
  • 否则,默认初始化
    只有类没有声明任何构造函数时,编译器才会自动生成默认构造函数
    如果类包含有内置类型或者复合类型的成员,只有这些成员都被赋予了类内初始值时,这个类才适合合成的默认构造函数
struct Sales_data{
    Sales_data() = default;
    Sales_data(const std::string &s) : bookNo(s){}
    Sales_data(const std::string &s, unsigned n, double p):
                 bookNo(s),units_sold(n), revenue(n*p){}
     Sales_data(std::istream &);
//其他已有的
    unsigned units_sold = 0;
    double revenue = 0.0;
};

Sales_data() = default;在C++11标准中,如果我们需要默认行为,可以用=default来要求编译器生成构造函数
对于第二个和第三个构造函数,在冒号和花括号之间新出现的部分,,称为构造函数初始化列表
当某个成员变量被初始化列表忽略时,他将以与合成默认构造函数相同的方式隐式初始化

c++Primer 第五版 第241页

类可以允许其他类或者函数访问它的非公有成员,方法是将其声明为类的友元(friend)
友元声明只能出现在类的内部,位置不限。不过一般来说,最好在类定义开始或者结束前的位置集中声明友元。

c++Primer 第五版 第242页 封装的益处

封装有两个重要的优点:

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

C++ Primer第五版 第242页 友元的声明

友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明,因此在友元声明之外需要再专门对函数进行一次声明
由于我们前面提到友元声明通常放在类同一个头文件中,因此我们的Sales_data头文件应该为友元函数在类外部提供声明(除了类内部以外)

c++ Primer 第五版 第245页 类内联函数

定义在类内部的成员函数是自动inline的
我们可以在类的内部将inline作为声明的一部分显式支出,也可以在类的外部用inline修饰其定义
虽然我们无需在声明和定义同时说明inline,但这样做是合法的。一般最好在类外部定义的地方说明inline,这使得类更容易理解
当然inline成员函数应该与相应的类定义在同一个头文件中

c++Primer 第五版第245页 可变数据成员

有时(但并不频繁)会有一种情况,我们希望修改类的某个数据成员,即使在const成员函数中,这时可以通过mutable关键字来实现

class Screen{
private:
    mutable size_t access_ctr;
public:
    void some_member() const;
};
void Screen::some_member() const{
    ++access_ctr;
}//const成员函数仍然可以修改mutablemember

c++Primer 第五版 第247页 从const成员函数返回*this

一个const成员函数如果以引用的形式返回*this,那他的返回类型将是常量引用
如果display是一个const成员,返回类型为const Screen&,这样其返回值无法被修改

Screen myScreen;
myScreen.display(cout).set('*');
//如果display返回常量引用,set将引发错误

通过区分成员函数是否是const的,我们可以对其进行重载。由于非常量版本的函数对常量对象不可用,所以我们只能在常量对象上调用const成员对象
在下面这个例子中,定义一个do_display进行实际工作,

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

和我们之前学的一样,一个成员调用另一个成员时,this被隐式的传递
小建议
为什么要费力定义一个单独的do_display函数呢?

  • 一个基本的原因是避免多处使用同一代码
  • 随着类的规模发展,display可能更复杂,相应操作只写一处作用较明显
  • 开发过程中可能给do_display加调试信息,最终版本被去掉,因此只定义一处更容易
  • 额外的调用不会增加任何开销,因为他在类内定义被隐式的声明为内联函数
    实践中,设计良好的c++代码常常包含大量类似于do_display的小函数,完成一组其他函数的“实际”工作。

c++Primer第五版第250页 类的声明

类似于函数,类也可以先声明,而暂时不定义

class Screen;//Screen类的声明

对于一个类来说,我们创建他的对象之前该类必须被定义,而不是仅仅被声明,但也有一种例外情况,当一个类的名字出现后,他就被认为被声明过(不是定义),因此类允许包含指向他自身类型的引用或指针:

class Link_screen{
    Screen window;
    Link_screen *next;
    Link_screen *prev;
};

c++Primer 第五版 第250页 友元

类可以把其他类定义为友元,也可以把其他类(之前定义过的)的成员函数定义成友元
如果一个类指定为友元类,那友元类的成员函数可以访问此类包括非公有成员在内的所有成员

class Screen{friend class Window_mgr;};//友元类
class Screen{friend void Window_mgr::clear(ScreenIndex);};//友元 类成员函数

注意友元关系不存在传递性
尽管重载函数名字相同,但仍然是不同的函数,因此一组重载函数的友元声明必须为每一个分别声明

c++ Primer 第五版 第253页 类的作用域

void Window_mgr::clear(ScreenIndex i){}

由于编译器在处理参数列表之前已明确了我们处于Window_mgr作用域中,所以不必专门说明ScreenIndex是Window_mgr类定义的
另一方面,函数返回类型通常在函数名之前。因此当类成员函数定义在类外部时,返回类型中使用的名字都位于类的作用域之外,这时需要指定他属于哪个类

class Window_mgr{
public:
    ScreenIndex addScreen(const Screen&);};
//首先处理返回类型
Window_mgr::ScreenIndex
Window_mgr::addScreen(const Screen &s){}

c++Primer 第五版 第254页 名字查找和类的作用域

一般来说内层作用域可以重新定义外层作用域中的名字,即使该名字在内层作用域中使用过
然而,在类中,如果成员使用了外层作用域中的某个名字,而这个名字代表一种类型,则类不能在之后重新定义该名字。

c++Primer 第五版第259页 构造函数初始化

构造函数初始化列表只说明用于初始化成员的值,但并不规定他们具体执行顺序
成员的初始化顺序和他们在类定义中出现的顺序一致
最好令构造函数初始值的顺序与成员声明的顺序一致,如果可能的话,尽量避免使用某些成员初始化其他成员

c++Primer第五版 第261页委托构造函数

c++11允许委托构造函数使用所属类的其他构造函数协助其进行构造

class Sales_data{
public:
    Sales_data(std::string s, unsigned cnt):bookNo(s),units_sold(cnt){}
    Sales_data():Sales_data("",0){}
    Sales_data(std::istream &is): Sales_data()
          {read(is,*this);}
};

c++ Primer 第五版第263页 隐式类类型转换

如果类的构造函数只接受一个实参,实际上定义了转换为该类型的隐式转换机制,有时我们称之为转换构造函数
而由于编译器只能自动地执行一步类型转换,因此

string null_book  = "9999";
item.combine(null_book);//会临时构造Sales_data转换null_book
//下面的代码隐式使用了两种转换,因此报错
item.combine("9999");
//把"9999"转换为string,再将string转为Sales_data
//如果想完成上述代码 可以
item.combine(string("9999"));//正确 显式string,隐式Salesdata
item.combine(Sales_data("9999"));//正确 隐式string,显式Salesdata

而在要求隐式转换的上下文中,我们可以通过explicit关键字进行阻止

class Sales_data{
    explicit Sales_dadta(const std::string &s):bookNo(s){}
    explicit Sales_data(std::istream&);
};
item.combine(null_book);//报错
item.combine(cin);//报错

关键字explicit支队一个实参的构造函数有效
尽管编译器不会将explicit构造函数用于隐式转换,但我们可以用于显式强制转换

item.combine(Sales_data(null_book));//正确
item.combine(static_cast<Sales_data>(cin));//正确

C++ Primer 第五版 第267页 字面值常量类

某些类可以是字面值常量类,它可能含有constexpr函数成员,这样的成员必须符合constexpr函数的所有要求,它们都是隐式const的
数据成员都是字面值类型的聚合类是一个字面值常量类
符合下列要求的费聚合类也是字面值常量类:

  • 数据成员都是字面值
  • 至少含有一个constexpr构造函数
  • 如果一个数据成员有类内初始值,则内置类型成员的初始值必须是常量表达式;或者成员属于某种类类型,初始值必须使用成员自己的constexpr构造函数
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象
    尽管构造函数不能是const的,但字面值常量类的构造函数可以是constexpr的
    constexpr构造函数可以声明成=default,或者删除函数的形式。构造函数必须既符合构造函数的要求(不能包含返回语句),又必须符合constexpr的要求(它能拥有的唯一可执行语句就是返回语句),这两点可知其函数体一般来说是空的
class Debug{
public:
    constexpr Debug(bool b=true):hw(b),io(b),other(b){}
/**/
private:
    bool hw,io,other;
};

constexpr构造函数补习初始化所有数据成员,初始值或使用constexpr构造函数或者一挑常量表达式

c++Primer 第五版 第269页 类的静态成员

static关键字使其成为类的静态成员,静态成员可以是public或者private的
类似的静态成员函数不与任何对象绑定在一起,不包含this指针,不能声明为const,且函数体内不能使用this
和类的所有成员一样,当我们只想类外部的静态成员时,必须指明所属的类。static关键字只出现在类内部的声明语句中。
静态数据成员不由类的构造函数初始化。而且一般来说,我们不在类内部初始化静态成员。相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次
类似于全局变量,一单定义,静态数据成员就一直存在于程序的整个声明周期中。

double Account::interestRate = initRate();
//定义并初始化一个静态成员

通常情况下类的静态成员不应该在类内初始化,但是我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr

class Account{
public:
    static double rate(){return interestRate;}
    static void rate(double);
private:
    static constexpr int period  = 30;//
};

c++Primer 第五版 第271页

静态成员独立于任何对象。因此在某些非静态成员非法的场合,静态成员却可以正常使用。例如,静态数据成员可以是不完全类型(249页),特别地,静态数据成员的可以就是他所属的类类型。而非静态成员受到限制,只能声明它所属的类的指针或引用:

class Bar{
 public:
//
private:
    static Bar mem1;//正确,静态成员可以  是不完全类型
    Bar *mem2;//正确 指针成员可以是不完全类型
    Bar mem3;// 错误,数据成员必须是完全类型
};
//另外一个区别是我们可以使用静态成员作为**默认实参**
class Screen{
    public:
        Screen& clear(char=bkground);
private:
        static const char  bkground;
};

非静态数据成员不能作为默认实参,因为他的值本身属于对象的一部分,这样做无法真正提供一个对象以便从中获取成员的值,从而引发错误

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容