C++ Primer学习

string::size_type sumLength(const string&, const string&);
string::size_type largerLength(const string&, const string&);
//根据其形参的取值,getFcn函数返回指向sumLength或者largerLength的指针
decltype(sumLength) *getFcn(const string&);

第2章 变量和基本类型

  1. 对象是指一块能存储数据并具有某种类型的内存空间。
  2. 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
  3. 如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为0,定义在函数体内部的内置类型变量将不被初始化。
  4. C++语言将声明和定义的区分开来。声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请了内存空间,也可能会为变量赋一个初始值。
  5. 如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示地初始化变量:
    extern int i; //声明i而非定义i
    int j;        //声明并定义j

     

  6. 引用的类型都要和与之绑定的对象严格匹配。而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。
  7. 引用的类型必须与其所引用对象的类型一致,但是有两个例外。第一个例外情况就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式:
    int i = 42;
    const int &r1 = i;     //允许将const int&绑定到一个普通int对象上
    const int &r2 = 42;    //正确:r2是一个常量引用
    const int &r3 = r1 * 2;//正确:r3是一个常量引用
    int &r4 = r1 * 2;      //错误:r4是一个普通的非常量引用

     

  8. 常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象可能是个非常量,所以允许通过其他途径改变它的值
    int i = 42;
    int &r1 = i;       //引用ri绑定对象i
    const int &r2 = i; //r2也绑定对象i,但是不允许通过r2修改i的值
    r1 = 0;            //r1并非常量,i的值修改为0
    r2 = 0;            //错误:r2是一个常量引用

     

  9. 顶层const(top-level const)表示指针本身是个常量,底层const(low-level const)表示指针所指的对象是一个常量。
  10. decltype的作用是选择并返回操作数的数据类型
    decltype(f()) sum; //sum的类型是f()的返回值类型
    const int ci = 0;
    decltype(ci) x = 0; //x的类型是const int;

     

  11. decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当variable本身就是一个引用时才是引用。
  12. 预处理变量,#define, #ifdef, #ifndef, #endif

第3章 字符串、向量和数组

  1. 头文件不应该包含using声明,因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。可能会产生始料未及的名字冲突。
  2. >>操作符会自动忽略开头的空白并从第一个真正的字符开始读起,直到遇见下一处空白为止(以空白分开),getline()函数从给定的的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(注意不存换行符)。
  3. 当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string:
    string s4 = s1 + ",";       //正确:把一个string对象和一个字面值相加
    string s5 = "hello" + ",";  //错误:两个运算对象都不是string
    string s6 = s1 + ", " + "world"; //正确:每个加法运算符都有一个运算对象是string
    string s7 = "hello" + "," + s2;  //错误:左边的加法是字面值+字面值

     

  4. C++语言中的字符串字面值并不是标准库类型string的对象。切记字符串字面值与string是不同的类型。
  5. C语言的头文件形如name.h,C++则将这些文件命名为cname,去掉了.h后缀。
  6. string::size_type, ptrdiff_t, size_t, difference_type
  7. 内置的下标运算符所用的索引值不是无符号类型,这一点与vector和string(标准库类型)不一样。
int ia[] = {0, 2, 4, 6, 8};
int *p = &ia[2];            //p指向索引为2的元素
int k = p[-2];              //p[-2]是ia[0]表示的那个元素

 

第4章 表达式

  1. 右值左值,左值可以位于赋值语句的左侧,右值则不能。
  2. 对于逻辑与运算符来说,当且仅当左侧运算对象为真时才对右侧运算对象求值。对于逻辑或运算符来说,当且仅当左侧运算对象为假时才对右侧运算对象求值。
  3. 复合赋值运算符(+=, -+, *=, /=, %= 算术运算符,<<=, >>=, &=, ^=, |=位运算符),都等价于a = a op b:使用复合运算符只求值一次,使用普通的运算符则求值两次。一次是作为右边子表达式的一部分求值,另一次是作为赋值运算的左侧运算对象求值。
  4. 后置递增运算符的优先级高于解引用运算符。
  5. 如果一条子表达式改变了某个运算对象的值,另一条子表达式又要使用该值的话,运算对象的求值顺序就很关键了。
    for(auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
        *it = toupper(*it);
    
    //该循环的行为是未定义的!
    while(beg != s.end() && !isspace(*beg))
        *beg = toupper(*beg++); //错误:该赋值语句未定义
    
    问题在于:赋值运算符左右两端的运算对象都用到了beg,并且右侧的运算对象还改变了beg的值,所以该赋值语句是未定义的。编译器可能按照下面的任意一种思路处理该表达式:
    *beg = toupper(*beg);       //如果先求左侧的值
    *(beg + 1) = toupper(*beg); //如果先求右侧的值
  6. 解引用运算符的优先级低于点运算符,所以执行解引用运算的子表达式两端必须加上括号。
  7. 嵌套条件运算符,允许在条件运算符的内部嵌套另外一个条件运算符。
    finalgrade = (grade > 90) ? "high pass" : (grade < 60) ? "fail" : "pass";
  8. 条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常需要在它两端加上括号。
    cout << ((grade < 60) ? "fail" : "pass"); //输出pass或者fail
    cout << (grade < 60) ? "fail" : "pass";  //输出1或者0
    cout << grade < 60 ? "fail" : "pass";    //错误:试图比较cout和60
  9. 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。
  10. 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。(sizeof(string) = 32; sizeof(vector) = 24)
  11. 隐式类型转换,在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针,当数组被用作decltype,或者取地址符(&)、sizeof及typeid等运算符的运算对象时,不会发生转换。
  12. 强制类型转换,static_castdynamic_castconst_castreinterpret_cast

     

第5章 语句

  1. 因为do while先执行语句或者块,后判断条件,所以不循序在条件部分定义变量
    do {
        mumble(foo);
    }while(int foo = get_foo()); //错误,将变量声明放在了do的条件部分

     

  2. 如果一段程序没有try语句块且发生了异常,系统会调用terminate函数并终止当前程序的执行。
  3. 异常发生时,调用者请求的一部分计算可能已经完成了,另一部分则尚未完成。通常情况下,略过部分程序意味着某些对象处理到一半就戛然而止,从而导致对象处于无效或未完成的状态,或者资源没有正确释放,等等。那些在异常发生期间正确执行了“清理”工作的程序被称作异常安全的代码。
  4. 4个头文件:, , ,

第6章 函数

  1. 形参函数体内部定义的变量称为局部变量
  2. 局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间及时对象所在的函数结束执行也不会对它有影响。
  3. 熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参替代指针。
  4. 拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本不支持拷贝操作。当某种类型不支持拷贝操作时,函数智能通过引用形参访问该类型的对象。如果函数无需改变引用形参的值,最好将其声明为常量引用。
  5. 一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。
  6. 当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的
    void func(const int i){/* func能够读取i,但是不能向i写值,既可以传入const int也可以传入int*/}
  7. 数组两个特性:不允许拷贝数组,以及在使用数组时会将其转换成指针。因为数组会被转换成指针,所以当我们为函数传递 一个数组时,实际上传递的是指向数组首元素的指针。
  8. main 处理命令行选项:当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。
  9. 如果函数的实参数量未知但是全部实参的类型都相同,可以使用initializer_list类型的形参。和vector不一样的是,initializer_list对象中的元素永远是常量值,我们无法改变initialzer_list对象中的值。
  10. 不要返回局部对象的引用或指针。函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域。
  11. main函数的返回值可以看做是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值得具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,可以使用者两个变量分别表示成功与失败。(EXIT_FAILURE/EXIT_SUCCESS) 
  12. 声明一个返回数组指针的函数:
    Type (*function(parameter_list)) [dimension];
    int (*func(int i)) [10];   //表示数组中的元素是int类型

     

  13. C++11标准写法,尾置返回类型
    //func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
    auto func(int i) -> int(*)[10];

     

  14. 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
  15. _ _func_ _ :存放函数的名字;_ _FILE_ _:存放文件名的字符创字面值;_ _LINE_ _:存放当前行号的整形字面值;_ _TIME_ _:存放文件编译时间的字符串字面值;_ _DATE_ _:存放文件编译日期的字符串字面值。
  16. 调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。(含有多个形参的函数匹配,参考书中6.6例子)
  17. 函数指针 
    bool lengthCompare(const string&, const string&);
    bool (*pf)(const string&, const string&); //未初始化
    
    pf = lengthCompare;
    pf = &lengthCompare; //等价
    
    bool b1 = pf("hello", "goodbye");
    bool b2 = (*pf)("hello", "goodbye");
    bool b3 = lengthCompare("hello", "goodbye");//等价
  18. 函数指针形参 和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。
    void useBigger(const string& s1, const string& s2,
                    bool pf(const string&, const string&));//第三个形参时函数类型,会自动地转换成指向函数的指针
    void useBigger(const string& s1, const string& s2,
                    bool (*pf)(const string&, const string&));
    
    typedef bool Func(const string&, const string&);
    typedef decltype(lengthCompare) Func2;//等价的类型
    
    typedef bool (*FuncP)(const string&, const string&);
    typedef decltype(lengthCompare) *FuncP2; //等价类型

     

  19. 返回指向函数的指针 
    using F = int(int*, int);    //F是函数类型,不是指针
    using PF = int(*)(int*, int);//PF是指针类型
    
    PF f1(int);    //正确:PF是指向函数的指针,f1返回指向函数的指针
    F f1(int);     //错误:F是函数类型,f1不能返回一个函数
    F *f1(int);    //正确:显示地制定返回类型是指向函数的指针
    
    int (*f1(int))(int*, int);
    //由内向外的顺序阅读这条声明语句:f1有形参列表,所以f1是个函数;f1前面有*,所以f1返回一个指针;进一步观察发现,指针的类型本身也是包含形参列表,因此指针指向函数,该函数的返回类型是int.
    auto f1(int) -> int (*)(int*, int); //尾置返回类型,等价
  20. decltype 作用于某个函数时,它返回函数类型而非指针类型。因此,显示地加上*表明我们需要返回指针,而非函数本身。

第7章 类

  1. IO类属于不能拷贝类型,因此需要通过引用来传递。
    istream& read(istream& is, Sales_data& item)
    {
        double price = 0;
        is >> item.bookNo >> item.units_sold >> price;
        item.revenue = price * item.units_sold;
        return is;
    }
    
    ostream& print(ostream& os, const Sales_data& item)
    {
        os << item.isbn() << " " << item.units_sold << " "
           << item.revenue << " " << item.avg_price();
        return os;
    }

     

  2. 只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。一旦定义了一些其他的构造函数,除非再定义一个默认的构造函数,否则类将没有默认构造函数。
  3. 如果类包含有内置类型或者复合类型的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。否则他们的值将是未定义的,合成的默认构造函数可能执行错误操作。
  4. 在C++11新标准中,如果我们需要默认行为,那么可以通过在参数列表后面写上 = default来要求编译器生成构造函数。如果=default 在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的。
  5. 使用classstruct定义类唯一的区别就是默认的访问权限。
  6. 友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。一般来说,最好在类定义开始或结束前的位置集中声明友元。
  7. 友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的外部)。
  8. 可以在类的内部把inline作为声明的一部分显式地声明成员函数,也能在类的外部用inline关键字修饰函数的定义
    class Screen{
    public:
        typedef std::string::size_type pos;
        Screen() = default;
        Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c){}
        char get() const{ return contents[cursor]; }  //隐式内联
        inline char get(pos ht, pos wd) const;        //显式内联
        Screen &move(pos r, pos c);                   //能在之后被设为内联
    private:
        pos cursor = 0;
        pos height = 0, width = 0;
        std::string contents;
    };
    
    inline Screen& Screen::move(pos r, pos c)         //可以在函数的定义处指定inline
    {
        pos row = r * width;
        cursor = row + c;
        return *this;
    }

     

  9. 一个可变数据成员(mutable data member)永远不会是const,即使它是const对象的成员。
    class Screen{
    public:
        void some_member() const;
    private:
        mutable size_t access_ctr; //即使在一个const对象内也能被修改
    };
    
    void Screen::some_member() const
    {
        ++access_ctr;
    }

     

  10. 返回*this的成员函数:下面例子如果定义的返回类型不是引用,则move的返回值将是*this的副本,因此调用set只能改变临时副本,不能改变myScreen的值。
    class Screen{
    public:
        Screen &set(char);
        Screen &set(pos, pos, char);
    };
    
    inline Screen &Screen::set(char c)
    {
        contents[cursor] = c;
        return *this;                    //将this对象作为左值返回
    }
    
    inline Screen &Screen::set(pos r, pos col, char ch)
    {
        contents[r * width + col] = ch;
        return *this;
    }
    
    Screen temp = myScreen.move(4, 0);
    temp.set('#');                      //不会改变myScreen的contents

     

  11. 一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用
  12. class Screen; 前向声明。在Screen声明之后定义之前是一个不完全类型。不完全类型只能在非常有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。只能声明不能定义,因为编译器不知道存储该数据成员需要多少空间。
  13. 一般来说,内层作用域可以重新定义外层作用域中的名字,即使该名字已经在内层作用域中使用过。然而在中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能再之后重新定义该名字
    typedef double Money;
    class Account {
    public:
        Money balance() { return bal; } //使用外层作用域的Money
    private:
        typedef double Money;    //错误:不能重新定义Money
        Money bal;
    }
  14. 如果没有在构造函数的初始值列表中显示地初始化成员,则该成员将在构造函数体之前执行默认初始化。(理解初始化和赋值的区别,成员是const或者引用的话,必须初始化
    Sales_data::Sales_data(const string &s, unsigned cnt, double price)
    {
        bookNo = s;
        units_sold = cnt;
        revenue = cnt * price;
    }
    
    //区别于初始化列表是:原来的版本初始化了它的数据成员,而这个版本是对数据成员执行了赋值操作。
  15. 如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值,所以类内的const和引用可以在构造函数的初始化列表里进行初始化
    class ConstRef
    {
    public:
        ConstRef(int ii):i(ii), ci(ii), ri(i){}
    private:
        int i;
        const int ci;
        int &ri;
    }

     

  16. 成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推。构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。如果一个成员是用另外一个成员来初始化的,那么这两个成员的初始化顺序就很关键了。
  17. 如果一个构造函数为所有参数都提供了默认实参,则实际上也定义了默认构造函数。
  18. 委托构造函数:使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他函数。
    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::istream &is):Sales_data() { read(is, *this); }
    }

     

  19. 当对象被默认初始化或值初始化时自动执行默认构造函数。默认初始化在以下情况下发生:a.当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时;b.当一个类本身含有类类型的成员且使用合成的默认构造函数时;c.当类类型的成员没有在构造函数初始值列表中显式地初始化时。
  20. 值初始化在以下情况下发生:a.在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时;b.当我们不使用初始值定义一个局部静态变量时;c.当我们通过书写形如T()的表达式显式地请求值初始化时,其中T是类型名。
  21. 隐式的类类型转换,能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
    string null_book = "9-9990-999";
    //用一个string的实参调用了Sales_data的combine成员,编译器用给定的string自动创建了一个Sales_data对象,新生成的这个临时Sales_data对象被传递给combine.
    item.combine(null_book); //item.combine(Sales_data(null_book));
    

     

  22. 抑制构造函数定义的隐式转换,将构造函数声明explicit加以阻止。关键字explicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于隐式转换,所以无须将这些构造函数指定为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::istream&);
    }

     

  23. 编译器不会将explicit的构造函数用于隐式转换过程,但是可以使用这样的构造函数显式地强制进行转换
    //正确:实参是一个显式构造的Sales_data对象
    item.combine(Sales_data(null_book));
    //正确:static_cast可以使用explicit的构造函数
    item.combine(static_cast(cin));

     

  24. 因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们不是由类的构造函数初始化的。一般来说,我们不能在类的内部初始化静态成员。必须在类的外部定义和初始化每个静态成员,一个静态数据成员只能定义一次。
  25. 静态数据成员可以是不完全类型,静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则受到限制,只能声明它所属类的指针或引用
    class Bar{
    public:
    private:
        static Bar mem1;  // 正确:静态成员可以是不完全类型
        Bar *mem2;  // 正确:指针成员可以是不完全类型
        Bar mem2;   // 错误:数据成员必须是完全类型
    };

     

  26.  

第8章 IO库

  1. IO对象无拷贝或赋值,因此不能将形参或返回类型设置为流类型,进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。
  2.  

你可能感兴趣的:(C++)