《C++Primer》读书笔记(三)

180 阅读16分钟

第6章 函数

局部静态对象

局部静态对象:在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

size_t count_calls() {
    static size_t ctr = 0;
    return ++ctr;
}

如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。

参数传递

  • 传值参数
  • 指针形参:指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:

void reset(int *ip) {
    *ip = 0; //改变指针ip所指对象的值
    ip = 0; // 只改变了ip的局部拷贝,实参未被改变
}
  • 传引用参数: 通过引用形参,允许函数改变一个或多个实参的值。

void reset(int &i) { //i事传给reset函数的对象的另一个名字
    i = 0; //改变了i所引用对象的值
}

使用形参返回额外信息

给函数传入一个额外的引用实参,令其保存字符出现的次数。

string::size_type find_char(const string &s, char c, string::size_type &occurs) {
    auto ret = s.size();
    ...
    return ret;
}

const形参和实参

当用实参初始化形参时会忽略顶层const。换句话说,形参的顶层const被忽略了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。


void fcn(const int i);
void fcn(int i); //错误,重复定义

数组形参

尽管不能以值的方式传递数组,但是我们可以把形参写成类似数组的形式:


void print(const int*);
void print(const int[]); //函数的意图时作用于数组
void print(const int[10]);//维度表示我们期望数组含有多少元素

含有可变形参的函数

如果所有的实参类型相同,可以传递一个名为initializer_list的标准库模型。函数的实参数量未知但是全部参数的类型都相同,我们可以使用initializer_list类型的实参。

void error_msg(initializer_list<string>i1) {
    for (...)
}
error_msg({"functionX", expected, actual});

使用尾置返回类型

auto func(int i) -> int(*)[10];

重载和const形参

Record lookup(Phone);
Record lookup(const Phone);//重复声明

如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载。

Record lookup(Account&);
Record lookup(const Account&);

内联函数和constexpr函数

将函数指定为内联函数(inline),通常就是将它在每个调用点上展开。


inline const string& shorterString(const string &s1, const string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

内联机制用于规模小、流程直接、频繁调用的函数。

constexpr函数是指能用于常量表达式的函数。

  • 函数的返回类型及所有形参都得是字面值类型
  • 函数体中有且只有一条return语句
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); //正确: foo是一个常量表达式

把内联函数和constexpr函数放在头文件中

函数匹配

  1. 选定本次调用对应的重载函数集,集合中的函数称为候选函数。(与被调用函数同名,声明在调用点可见)
  2. 从候选函数中选出能被这组实参调用的函数,这些选出的函数为可行函数。(其形参数量与本次调用提供的实参数量相等,每个实参的类型与对应的形参类型相同或能转换成形参的类型)
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); //调用 void f(double, double);
f(42, 2.56); //二义性报错

函数指针

函数指针返回的是函数,并非对象。


bool lengthCompare(const string &, const string. &);
bool (*pf)(const string &, const string. &); //未初始化

把函数名作为一个值使用时,改函数自动转换成指针。

pf = lengthCompare; //pf指向名为lengthCompare的函数
pf = &lengthCompare; //等价的赋值语句:取地址符是可选的

直接使用指向函数的指针调用该函数,无需提前解引用指针


bool b1 = pf("hello", "goodbye");//调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye"); //等价调用
bool b3 = lengthCompare("hello", "goodbye"); //等价调用

虽然不能定义函数类型的形参,但是形参可以是指向函数的指针

void useBigger(const string &s1, const string &s2, bool pf(const string &, const sting &));
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const sting &));
useBigger(s1, s2, lengthCompare);

返回指向函数的指针


using F = int(int*, int);
using PF = int(*)(int*, int);
PF f1(int); //正确,PF是指向函数的指针,f1返回指向函数的指针。
F *f1(int); //正确:显示的指定返回类型时函数的指针

第7章 类

定义成员函数

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;
}
//Sales_data的非成员接口函数
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);

成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this.

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

因为this的目的总是指向“这个”对象,所以this是一个常量指针,我们不允许改变this中保存的地址。

引入const成员函数

isbn函数的另一个关键之处是紧随参数列表之后的const关键字,这里,const的作用是修改隐式this指针的类型。

默认情况下this的类型是指向类类型非常量版本的常量指针。紧跟在函数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数。isbn可以读取调用它的对象的数据成员,但是不能写入新值

在类的外部定义成员函数

如果成员函数被声明成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性。同时,类外部定义的成员的名字必须包含它所属的类名:


double Sales_data::avg_price() const {
    if (units_sold)
        return revenue/units_sold;
    else
        return 0;
}

定义一个返回this对象的函数

函数combie的设计初衷类似于复合赋值运算符+=,调用该函数的对象代表的是赋值运算符左侧的运算对象,右侧运算对象则通过显式的实参被传入函数:


Sales_data& Sales_data::combine(const Sales_data &rhs) {
    units_sold += rhs.units_sold; 把rhs的成员加到this对象的成员上
    revenue += rhs.revenue;
    return *this; //返回调用该函数的对象
}

定义类相关的非成员函数

定于非成员函数的方式与其他函数一样,通常把函数的声明和定义分离开来。如果函数在概念上属于类但是不定义在类中,则它一般应与类声明在同一个头文件内。

构造函数

每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

构造函数的名字和类名不同。和其他函数不一样的是,构造函数没有返回类型;除此之外类似于其他的函数,构造函数也有一个(可能为空的)参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。

不同于其他成员函数,构造函数不能被声明成const的。

类通过一个特殊的构造函数来控制默认初始化的过程,这个函数叫做默认构造函数。默认构造函数无须任何实参。编译器所创建的构造函数又被称为合成的默认构造函数。

合成默认构造函数的规则

  • 如果存在类内的初始值,用它来初始化成员。
  • 否则,默认初始化该成员。

在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上 = default 来要求编译器生成构造函数,其中= default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。
和其他函数一样,如果 = default 在类的内部,则默认构造函数是内联的;如果在类的外部,则该成员默认情况下不是内联的。

构造函数初始值列表:

Sales_dat(const std::string &s): bookNo(s), units_sold(0), revenuw(0) {}

在类的外部定义构造函数:

Sales_data: Sales_data(std:: istream &is) {
    read(is, *this);
}

这个构造函数没有构造函数初始值列表,它的构造函数初始值列表是空的。尽管构造函数初始值列表是空的,但是由于执行了构造函数体,所以对象的成员仍能被初始化。

访问控制与封装

我们使用访问说明符加强类的封装性:

  • 定义在public说明符之后的成员在整个程序内可以访问,public成员定义类的接口。
  • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。

使用struct或class关键字

可以使用这两个关键字中的任何一个定义类。唯一的不同是,struct和class的默认访问权限不一样大。

  • 类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式
  • 我们使用struct关键字,定义在第一个访问说明符之前的成员是public的;相反,使用class关键字,这些成员是private的。

友元

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

class Sales_data {
    friend Sales_data add(const Sales_data&, const Sales_data&);
    friend std::istream &read(std::istream&, Sales_data&);
    friend std::ostream &print(std::ostream&, const Sales_data&);
 public:
     Sales_data() = dafault;
     Sales_data(const std:: string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n) {}
     Sales_data(const std:: string &s): bookNo(s) {}
     Sales_data(std::istream&);
     st:string isbn() const { return bookNo; }
     Sales_data &combine(const Sales_data&);
private:
    std::string bookNol
    unsigned units_sold = 0;
    double revenue = 0.0;
};
//Sales_data接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);

友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。

友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元的声明之外再专门对函数进行一次声明。

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。

class Screen {
    friend class Window_mgr;
}

除了令整个window_mgr作为友元之外,Screen还可以只为clear提供访问权限。当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类:


class Screen {
    friend void Window_mgr::clear(ScreenIndex);
}

定义一个类型成员

除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制,可以是public或者private的一种;

class Screen {
public: 
    typedef std::string::size_type pos;
    // using pos =  std::string::size_type;
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
};

虽然我们无须在声明和定义的地方同时说明inlie,但是这么做其实是合法的。不过,最好只在类外部定义的地方说明iline。

可变数据成员

我们希望能修改类的某个数据成员,即使是在一个const成员函数内。可以通过在变量的声明中加入mutable关键字做到这一点。
一个可变数据成员永远不会是const的,即使它是const对象的成员。

返回* this的成员函数


class Screen {
public:
    Screen &set(char);
    Screen &set(pos, pos, char);
};
iline Screen &screen::set(char c) {
    contents[cursor] = c;
    return *this;
}
iline Screen &screen::set(pos r, pos col, char ch) {
    contents[r * width + col] = ch;
    return *this;
}

和move操作一样,我们的set成员的返回值是调用set的对象的引用。返回引用的函数是左值的,意味着这些函数返回的是对象本身而非对象的副本。

myScreen.move(4,0).set("#");

一个cosnt成员函数如果以引用的形式返回* this,那么它的返回类型是常量引用。

类类型

class Screen;

这种声明有时被称作前向声明,它向程序引入了名字Screen并指明Screen是一种类类型。对于类型Screen来说,在它声明之后定义之前是一个不完全类型,也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员。
不完全类型只能在非常有限的情景下使用: 可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。

委托构造函数

C++11 新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把自己的一些(或全部)职责委托给了其他构造函数。

class Sales_data {
public:
    //非委托构造函数使用对应的实参初始化成员
    Sales_data(std::string s, unsigned cnt, double price): bookNo(s), units_sold(cnt), revenue(cnt *price) {}
    //其余构造函数全部委托给另一个构造函数
    Sales_data(): Sales_data("", 0, 0) {}
    Sales_data(std:string s): Sales_data(s,0,0) {}
    Sales_data(std::string &is): Sales_data() { read(is, *this); }
}

在这个Sales_data类中,除了一个构造函数外其他的都委托了它们的工作。

抑制构造函数定义的隐式转换

在要求隐式转换的程序上下文中,我们可以通过将构造函数声明位explicit加以阻止。

class Sales_data {
public:
    Sales_data() = default;
    Sales_data(const std::string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p * n) {}
    explicit Sales_data(const std::string &s): bookNo(s) {}
    explicit Sales_data(std::isstream&);
}

此时,没有任何构造函数能用于隐式地创建Sales_data对象,之前的两种用法都无法通过编译。

item.combine("99999"); //错误:string构造函数时explicit的

关键字expicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须讲这些构造函数指定为explicit的。
只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复。

发生隐式转换的一种情况时当我们执行拷贝形式的初始化时(使用=)。此时,我们只能使用直接初始化而不能使用explicit构造函数;

Sales_data item1(null_book);//正确直接初始化
Sales_data item2 = null_book; //错误: 不能将explicit构造函数用于拷贝形式的初始化过程

聚合类

当一个类满足如下条件时,我们说它是聚合的:

  • 所有成员都是public的
  • 没有的定义任何构造函数
  • 没有类内初始值
  • 没有基类,也没有vitural函数
struct Data {
    int ival;
    string s;
}

字面值常量类

除了算术类型、引用和指针外,某些类也是字面值类型。和其他类不同,字面值类型的类可能含有constexpr函数成员。这样的成员必须符合constexpr函数的所有要求,它们是隐式const的。

数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下述要求,则它是一个字面值常量类:

  • 数据成员都必须是字面值类型
  • 类必须至少含有一个constexpr构造函数
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一个常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象

constexpr构造函数:

尽管构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数。事实上,一个字面值常量类必须至少提供一个constexpr构造函数。

类的静态成员

我们通过在成员的声明之前加上关键字static使得其与类关联在一起。和其他成员一样,静态成员可以是public的或private的。静态数据成员的类型可以是常量、引用、指针、类类型等。

class Account {
publicvoid caculate() { amount += amount * interestRate; }
    static double rate() { return interestRate; }
    static void rate(double);
private: 
    std::string owner;
    double amount;
    static double interestRate;
    static double initRate();
};

类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。

类似的,静态成员函数也不与任何对象绑定在一起,他们不包含this指针。作为结果,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针。


double r;
r = Account::rate();

虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员:

Account ac1;
Account *ac2 = &ac1;
r = ac1.rate(); //通过Accountde 的对象或引用
r = ac2->rate(); //通过指向Account 对象的指针

成员函数不用通过作用域运算符就能直接使用静态成员:

class Account {
public:
    void caculate() { amount += amount * interestRate; }
private:
    static double interestRate;
};

定义静态成员:

和其他的成员函数一样,我们既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员,不能重复static关键字,该关键字只出现在类内部的声明语句:

void Account::rate(double newRate) {
    interestRate = newRate;
}

静态成员的类内初始化:

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


class Account {
public:
    static double rate() { return interestRate; }
    static void rate(double);
private:
    static constexpr int period = 30;// period是常量表达式
    double daily_tbl[period];
};

静态成员能用于某些场景,而普通成员不能

class Bar {
public:
    // ...
private:
    static Bar mem1; //正确:静态成员可以是不完全类型
    Bar *mem2;  //正确:指针成员可以是不完全类型
    Bar mem3; //错误:数据成员必须是完全类型
};

静态成员和普通成员的另外一个区别是我们可以使用静态成员作为默认实参:

Class Screen {
public:
    Screen& clear(char = bkground);
private:
    static const char bkground;
    
};

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

第8章 IO库

IO类

类型ifstream和isstringstream都继承于isstream。

IO对抗无拷贝或赋值

ofstream = out1, out2;
out1 = out2; //错误:不能对流对象赋值
ofstream print(ofstream); //错误:不能初始化ofstream参数
out2 = print(out2);  //错误: 不能拷贝流对象

由于不能拷贝IO对象,因此我们也不能将形参或返回类型置为流类型。进行IO操作的函数通常以引用方式传递和返回流。

读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

管理输出缓冲

每个输出流都管理一个缓冲区,用来保存程序读写的数据。

文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。

导致缓冲区刷新(即,数据真正写到输出设备或文件)的原因有很多:

  • 程序正常结束,作为main函数的return操作的一部分,缓冲区刷新被执行。
  • 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区
  • 我们可以使用操作符如endl来显式刷新缓冲区。
  • 在每个输出操作之后,我们可以用操作符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的。
  • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如:默认情况下,cin和cerr都关联到cout。因此读cin或写cerr都会导致cout的缓冲区被刷新。
cout << "hi!" << endl; //输出hi和一个换行,然后刷新缓冲区
cout << "hi!" << flush; //输出hi,然后刷新缓冲区,不附加任何额外字符
cout << "hi!" << ends; //输出hi和一个空字符,然后刷新缓冲区。

cout << unitbuf; //所有输出操作后都会立即刷新缓冲区
//任何输出都立即刷新,无缓冲
cout << nounitbuf; //回到正常的缓冲方式

文件输出和输出

当我们想读写文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个名为open的成成员函数,完成一些系统相关的操作。

创建文件流对象时,我们提供文件名。如果提供了一个文件名,open会自动调用。

ifstream in(ifile); //构造一个ifstream并打开给定文件
ofstream out; //输出文件流未关联到任何文件

如果有一个函数接受一个ostream&参数,我们在调用这个函数时,可以传递给它一个ofstream对象,对istream&和ifstream也是类似的。

ifstream input(argv[1]); //打开销售记录文件
ofstream output(argv[2]); //打开输出文件
Sales_data total; //保存销售总额的变量
if (read(input, trans)) { //读取第一条销售记录
    Sales_data trans; //保存下一条销售记录的变量
    while(read(input, trans)) {
        ...
    }
    print(output, total) << endl;
} else {
    cerr << "No data?!" << endl;
}

打开关闭文件

ifstream in(file); //构筑一个ifstream并打开给定文件
ofstream out; //输出文件流未与任何文件想关联
out.open(ifile + ".copy"); //打开指定文件

in.close(); // 关闭文件
in.open(ifile + "2"); //打开另一个文件

string流

sstream 头文件定义了三个类型来支持内存IO,这些类型可以向string写入数据,从string读取数据,就像string是一个IO流一样。

isstringstream读取数据

string line, word; //分别保存来自输入的一行和单词
vector<PersonInfo> people; //保存来自输入的所有记录
//逐行从输入读取数据,直至cin遇到文件尾
while(getline(cin,line)) {
    PersonInfo info; //创建一个保存此记录数据的对象
    istringstream record(line); //将记录绑定到刚读入的行
    record >> info.name; //读取名字
    while(record >> word) 
        info.phones.push_back(word);
    people.push_back(info); 
}