C++ Chap1

1.2变量和基本类型

  • 1.2
    • 基本知识
      • 一个字节=8位
      • image.png
        其它类型补充:long long 至少64位
        无符号型:unsigned…
        • 各类型最大值表示常量,最小是:min

          int float double long long long unsigned long long
          INT_MAX FLT_MAX DBL_MAX LONG_MAX LONG_LONG_MAX ULONG_LONG_MAX
        • 当int是4字节即32位时,最大数为(2^31)-1=2147483647,是10位数

    • 类型转换
      • 浮点数->整数:结果仅保留浮点数中小数点之前部分
      • 整数->浮点数:小数部分记为0,可能存在整数所占空间超过浮点数中整数可用位置,导致精度损失
      • 无符号->有符号:超出范围时,得到初始值对无符号类型取模后的余数(与负数相加后的值)
      • 更多在1.3表达式中介绍 类型转换
    • 常量:不可被改变的量
      • 字面值常量
        • 字符字面值:单引号包围
        • 字符串字面值:双引号包围,注意字符串字面值的长度比看起来要长,因为自动在结尾加了‘\0’
        • 整型常量、浮点型字面值:数字;其中以0开头位八进制数。以0x或0X开头为十六进制
        • 布尔常量:true与false
      • 符号常量:自定的常量 ,使用const限定符限制;const对象仅在文件内有效,多文件共享必须添加extern关键字
        1
        const 类型 常量名 = 初始化值;
      • 转义符号:以反斜杠开始
      • image.png
    • 变量
      • 变量的声明和定义
        • 声明:规定了变量的类型和名字
          对于全局变量,作用域跨越不同源程序文件,可以仅声明而不定义,使用关键字extern
          extern 数据类型 全局变量名;
          但是静态全局变量的作用域仅限于定义其的原程序文件中
        • 定义:申请储存空间,可能为变量赋初始值,若不赋初始值,则进行默认初始化
          • 变量定义的五要素
            • 1.储存类型(决定生命期以及可见性,在某函数之内:auto:局部自动变量,可省略 static:静态局部变量;在所有函数之外:static:静态全局变量,如有缺省就是全局变量)
            • 2.数据类型(决定变量的储存格式以及取值范围)
            • 3.变量名
            • 4.变量初始值
            • 5.变量地址(系统确定)
        • 多文件中:变量的定义只能出现在一个文件中,只能被定义一次但是可以被多次声明
      • 变量的储存类型
        • 储存类型决定了变量的生命期和可见性
        • 内存区域:
          1.代码区;
          2.全局数据区、常量池(存放已经初始化的全局变量、静态变量以及常量数据);
          3.栈区(存放局部变量);
          4.堆区(new的空间,自行申请)
        • 全局变量:在所有函数之外定义的变量
          • 全局变量的定义应在某一.cpp文件中,在其他访问该变量的文件中需要声明(extern)
        • 静态全局变量:在所有函数之外定义储存类型为static的变量;作用域仅在其所在的编译文件(即只能在一个文件中使用)
        • 静态局部变量:在某函数内定义为static的变量,存放在全局数据区,其它函数难以访问该变量,当再次调用该函数时,静态局部变量才可被使用,同时跳过该变量的定义以及初始化(静态局部变量最多可悲定义以及初始化一次)
        • 局部自动变量:储存类型为auto的变量(auto可被省略),存放在栈空间中
      • 复合类型
        • 引用:实际上是给一个已经存在的对象起了另一个名字;
          1
          2
          3
          数据类型 & 引用名 = 已存在变量名;  //&是一个标记符号
          //常量的引用(即对const的引用)
          const 数据类型 & 引用名 = 常量名或一个已经存在的变量;
          • 解释:对于常量引用
            • 1.直接引用常量
            • 2.引用一个已经存在的变量,此时不可以通过引用修改它的值,但是可以通过其他途径修改
          • 引用类型的初始值一定是一个对象,不能是一个字面值
          • 不能对引用进行引用,因为引用本身不是一个对象
          • 引用即绑定,引用初始化后不可以绑定到另外一个对象
          • 引用类型与引用的对象类型不一致的情况:
            • 1.初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用类型即可(注意,引用不能绑定存在常量引用的表达式,如r1是一个常量引用,一个非常量引用r2不能绑定含有r1的表达式作为初始值)书p55
            • 2.允许一个常量引用绑定非常量对象、字面值甚至一般表达式
        • 指针
          • 初始化一个指针:通过取地址符&获取某一地址赋给指针 ;注意,此时如果不初始化为特定的值,则指针会随机指向一个地址,而不是NULL
            类型名 *指针名 = &某一变量/常量地址;
            • 指针指向某个对象时,可以使用解引用符加指针名来表示指针指向的对象
              此时可以通过
              指针名的形式修改、访问指针指向对象的值
          • 初始化一个指针,并将该指针与另一个指针指向同一个对象
            1
            2
            3
            4
            5
            6
            7
            8
            int i=0,j=0;
            int *p = &i;//这里*是指针符号,代表p是一个指向int类型的指针
            int *pi;
            pi = p;//直接使用等号,这里交换的是地址
            pi = &j;//也可以再次修改
            cout<<p<<" "<<(*p);
            //前一个输出的是p的地址,后一个输出的是p指向的内容,此时*为解引用,返回p所指对象的值
            //也就是同理为什么*(a)能表示数组的a[0]
          • 注意 * 符号只能修饰一个变量名,即不能将int*理解为一种类型
          • 指针的指针:使用多个* 来表示不同级别的指针;可以对不同级的指针进行引用,但是不能定义指向引用的指针(因为引用不是对象)、
          • void*类型的指针:可以存放任意对象的地址,但是不能访问该指针指向的对象。
          • const与指针
            • 1.指向常量的指针(const 类型名 *指针名):可以用于存放常量的地址(常量地址只能用指向常量的指针存放),也可以用于存放一个变量的地址,但是此时不可以通过这个指针改变相应变量的值,即指针所指的对象值不能被改变
            • 2.const指针(类型名 *const 指针名):指针本身为常量,可以通过指针修改指针指向对象的值,但是不可以更改指向的对象,即指针储存的地址不能被改变
            • 顶层const与底层const(书p57)
              • 顶层const作用于对象本身
              • 顶层const【const指针】:指针本身是一个常量,除之阵外,顶层const可以表示任意的对象是常量(如算术类型、类等)
              • 底层const【指向常量的指针、const引用】:指针指向的对象是一个常量,与指针、引用等复合类型的基本类型部分有关
              • 要拷贝底层const的值,要么拷贝者也为底层const,要么将拷贝者转换成常量(底层const)。
              • 对于顶层const的拷贝:由于拷贝不改变对象的值,因此拷入拷出的对象是否为常量都无影响。
                1
                2
                3
                4
                5
                6
                int i = 0;
                int *const p1 = &i; // 不能改变 p1 的值,这是一个顶层
                const int ci = 42; // 不能改变 ci 的值,这是一个顶层
                const int *p2 = &ci; // 允许改变 p2 的值,这是一个底层
                const int *const p3 = p2; // 靠右的 const 是顶层 const,靠左的是底层 const
                const int &r = ci; // 所有的引用本身都是顶层 const,因为引用一旦初始化就不能再改为其他对象的引用,这里用于声明引用的 const 都是底层 const
        • constexpr和常量表达式
          • 常量表达式是指不会改变并且在编译过程中就能得到计算结果的表达式,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式
          • 判断是否为常量表达式:由数据类型和初始值共同决定
            • image.png{:height 76, :width 446}
            • image.png
            • staff_size:初始值虽然是字面值常量,但数据类型时普通int,不是const int,不属于常量表达式
            • sz本身是常量,但它的具体值直到运行后才能获取到,所以不是常量表达式
          • constexpr变量
            • 复杂系统中,很难分辨一个初始值是不是常量表达式,C++11中规定可以将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式,声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化
              • image.png
            • 尽量不要使用普通函数作为constexpr变量的初始值,但是可以定义相应constexpr函数初始化constexpr变量
          • 字面值类型
            • 常量表达式的值需要在编译时就得到计算,因此声明constexpr时用到的类型一般是字面值类型,如算术类型、引用类型和指针;而自定义类、IO库、string类型则不属于字面值类型,也不能被定义为constexpr。(其它类型字面值, 未定 p267、736)
            • 普通函数体内定义的变量一般并非存在固定的地址中,因此constexpr指针不能指向这样的变量
              constexpr指针可以指向的变量:
              1.定义于所有函数体之外的对象,其地址固定不变,能用来初始化constexpr指针。
              2.允许函数定义一类有效范围超过函数本身的变量,这类变量和定义在函数之外的变量也有固定地址,constexpr引用也可以绑定到这样的变量上,constexpr指针也可以指向这样的变量
          • 指针和constexpr
            • constexpr声明中如果定义了一个指针,那么限定符constexpr仅对指针有效,与指针所指的对象无关
              • image.png
              • constexpr将所定义的对象置为了顶层const
            • 与其他常量指针类似,constexpr指针既可以指向常量,也可以指向一个非常量
              • image.png
    • 对于类型的处理
      • 类型别名 typedef
        • 定义方法
          • 1.使用关键字typedef typedef 类型名 别名;(类型名可以包含*与&)
            • image.png
          • 2.使用别名声明 using 别名 = 类型名;
          • 3.关于指针、常量与类型别名c++primer p61
            • 如果使用类型别名指代符合类型或常量需要特别注意
              1
              2
              3
              4
              5
              typedef char *pstring;//指向字符的指针的别名
              const pstring cstr = 0;//cstr是一个指向char的常量指针
              //不是指向常量字符的指针
              const char *cstr = 0;//与上行代码不同
              const pstring *ps;//ps是个指针,它的对象是指向char的常量指针

              2、4行代码分析:
              第二行:是一个指向char的常量指针
              第四行:指向const char的指针
      • auto类型说明符 未定
      • decltype类型指示符 未定
  • 头文件
    • 头文件包含只能被定义一次的实体,如类、const、constexpr变量
    • 使用头文件保护符
      1
      2
      3
      #define 把一个名字设为预处理变量
      #ifndef 当且仅当变量未定义时为真
      #ifdef 当且仅当变量已定义时为真
    • 头文件中不应该包含using声明

1.3字符串、向量和数组

  • String
    • 初始化string对象的方式
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      string s1;//初始化一个s1空串
      string s2(s1);//s2是s1的副本;这里也可以是C-字符串的副本
      string s2 = s1;//与上一句等价
      string s(str,stridx); //将字符串str内“始于位置stridx”的部分当作字符串的初值
      string s(str,stridx,strlen); //将字符串str内“始于stridx且长度顶多strlen”的部分作为字符串的初值
      string s(chars,chars_len); //将C字符串前chars_len个字符作为字符串s的初值。
      string s3("value");//s3是字面值"value"的副本,除字面值最后的空字符除外
      string s3 = "value";//效果与上同
      string s4(n, 'c');//s4初始化为n个由c组成的串
      string s(beg,end); //以区间beg;end(不包含end)内的字符作为字符串s的初值'
      s.~string() //销毁所有字符,释放内存

      使用”=“执行拷贝初始化;不用”=“则是直接初始化;当初始值只有一个时,二者皆可用,但对于要用到多个值时应采用直接初始化(上例s4)
    • string的使用场景
      • 读入数量未知的字符串
      • 使用getline读取一行getline(流,string字符串,中止符);将换行符丢弃
    • string对象的赋值、改值、比较操作
      • string的内容比较
        • 可以直接使用==或!=检查两字符串是都相同
        • 可以直接使用比较关系运算符(<,>,<=,>=)进行string字符串大小的比较
        • str.empty()检查是否为空
          str.length()返回string的长度
          str.size()也可以返回string的长度
      • 可以支持’='相互赋值
      • 可以使用下标访问并更改字符串中部分值;需要考虑下标的范围0-s.size()
        访问字符时应先检查字符串是否为空,即需要先使用str.size()
      • string类型的相加
        • 可以使用+,+=对string对象进行相加,即在字符串后接长字符串
          • 字面值与string对象相加:需要确保每个加法运算符两侧的运算对象至少有一个是string,即两个字面值不可以进行相加
            但对于如string s = s1 + "," + "world";可以看作string s = (s1 + ",") + "world";
            实际合法
      • 使用范围for对每一个字符进行处理(C++11)
        • 范围for一般使用法;注意,进行一般范围for的使用时,循环处理的数组的值只是被拷贝到范围变量中,对于范围变量的修改不能实际改变数组中的内容
          1
          2
          for (dataType rangeVariable : array)
          statement;
          dataType 范围变量的数据类型,即需要遍历的数组的类型;序列中每个元素都能转换成该变量的类型
          rangeVariable 范围变量的名称,在循环期间接收相应元素的值;如第一次循环中接受下标为0的元素的值可以使用auto指定变量的数据类
          -此处冒号前语句:定义一个变量,可以使用auto,如果需要对序列中元素执行写操作,则必须声明成引用类型
          array 循环处理的数组名称,必须是一个序列,(特点拥有可以返回迭代器begin和end成员)对数组中的每个元素迭代一次;范围for内不能对vector进行增减的实际原因是在此处范围for内已经预留了begin和end的值
          statement 每次循环迭代期间需要处理的语句,多语句可以使用大括号包围
        • 若要使用范围for对数组进行修改,则需要将范围变量声明为引用(即&rangeVariable)(由于引用变量时其他值的一个别名,因此可以使用该引用直接修改数组内相关值)
        • 基于范围的for循环使用情况:不需要使用元素下标的情况,不访问两个或两个以上不同数组元素的情况
        • 参考:C++基于范围的for循环详解 (biancheng.net)
      • string类函数对于字符串的增、删、改、查;使用时需要用到.这一成员符号
        • 插入字符串:insert()函数 原型: string& insert (size_t pos, const string& str);
          pos:要插入的位置,即要插入的内容头元素在的下标
          str:要插入的字符串,对于string以及C-字符串都适用
        • 删除字符串:erase()函数 原型:string& erase (size_t pos = 0, size_t len = npos);
          可以删除一个子串,不必担心越界,函数最多删到字符串结尾
          pos:要删除的位置;len:要删除的长度,若不写则直接删完,即len=s.length()-pos
        • 提取子串:substr()函数 原型:string substr (size_t pos = 0, size_t len = npos) const;
          pos:要提取的子串起始下标;len:要提取的子串长度
          pos越界,则异常;len越界,则最多取到字符串尾
        • 查找固定字符串的位置:find()函数 原型:size_t find (const string& str, size_t pos = 0) const;``size_t find (const char* s, size_t pos = 0) const;
          查找的字符串可以是c风格,也可以是string;pos:查找的起点
          返回子字符串第一次出现在字符串中的起始下标,若未查找到,则返回一个无穷大4294967295
        • 查找固定字符串的位置:rfind()
          第二个参数pos规定了查找的末尾,而在find()函数中查找的开始位于pos
        • 查找子字符串和字符串共同具有的字符在字符串中首次出现的位置:find_first_of()
          e.g.s1.find_first_of(s2);
    • 对于字符串中每一个字符的检测、更改(大小写)的函数
      • image.png
    • string类型输入与输出,使用>>与<<
    • string与C-字符串
      • string转换为C-字符串:使用函数c_str(),直接返回相应的C-字符串的const指针(const char*);也可以使用data();区别:前者有’\0’,后者无
      • C-字符串转换成string类型:直接使用string s2(s1);创建
        copy()则把字符串的内容复制或写入既有的c_string或字符数组内
  • 标准库类型vector 需要头文件<vector>
    • 特点:顺序序列、动态数组、能够感知内存分配器
    • 类模板简述:模板本身不是类或者函数,相反可以将模板看作编译器生产类或函数编写的一份说明。编译器需要根据模板创建类或函数,这一过程称为实例化,使用模板时需要指出编译器应该把类或函数实例化的类型
      • 对于类模板来说,通过提供一些额外信息来指定模板到底实例化什么样的类,需要提供哪些信息由模板决定。提供信息的方式:在模板名字后跟一堆尖括号,在尖括号<>内指定信息
    • vector容器的初始化
      • 一维vector的初始化:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        vector<T> v1;//创建一个空的vetor,元素是T类型
        vector<T> v2(v1);//v2中包含v1所有元素的副本 两个vector对象的类型必须要相同
        vector<T> v2 = v1;//同上
        //创建指定数量元素
        vector<T> v3(n,val);//v3包含n个重复的元素,每个都是val
        vector<T> v4(n);//v4包含了n个重复地执行力初始化的对象;
        //若为int,默认为0,若为string 默认为空字符
        //列表初始化;初始化类型值一定要与vector相同
        vector<T> v5{a,b,c...};//v5包含了初始值个数的元素,每个元素有相应初始值
        vector<T> v5 = {a,b,c...};//同上
        //在无法进行列表初始化时,编译器会尝试使用默认初始化(即将{}翻译为())
        vector(begin,end);//复制[begin,end)区间内另一个数组的元素到vector中
        //此处例子:
        int iArray[]={2,4,6};
        vector<int> vecIntD(iArray,iArray+3);
        默认初始化:通过元素数量来初始化vector对象,即构造vector对象,使用圆括号,说明vector的容量与元素初值
        列表初始化:使用花括号
      • 二维vector的初始化:vector< vector<int> > v(n,vector<int>(m));
        声明时确定内外层vector的大小,包含n个有m个元素的vector
        在非C++11中,二维数组的外层vector右侧尖括号要一个空格
    • 向vector中添加元素 使用成员函数push_back()
      vector不支持以下标形式添加元素,下标运算符仅仅可以访问已经存在的元素
    • vector支持的操作
      • image.png{:height 252, :width 590}
    • vector也可以使用范围for进行查找访问
    • vector与C-字符串
      • 使用C-字符串初始化vector:需要指明拷贝数组的首地址与尾后地址
        • 1.使用标准库函数vector<int>(begin(array),end(array));
        • 2.使用指针(这里使用数组名作为指针)vector<int>(array+1,array+4);
        • 拷贝的元素可以是数组的一部分
  • 迭代器(iterator/ɪtə’reɪtə/)
    • 迭代器可以用auto关键字定义
      auto b = v.begin();
      典型迭代器:begin() end()成员函数
    • 迭代器类型:(一般来说无需知道),直接用auto自动识别;
      有const可支持只读;对于begin、end函数,返回值是否为常量是根据对象是否为常量决定的,为了得到常量迭代器,可以使用C++11中新函数cbegin与cend
    • 迭代器运算符
      image.png
    • 迭代器的算术运算
      • 迭代器与一个整数值加减返回值是向前/后移动了若干个位置的迭代器,相加减后迭代器会指向原对象中的一个元素或是指向尾元素的下一位置(即end())
      • 若两个元素都指向同一个容器中的元素或尾元素的下一位置,则二者可以想见,所得结果是两个迭代器的距离,(类型名为difference_type的带符号整型数)
      • 应用 :二分法
    • 迭代器的应用
      • 使用迭代器检查是否非空:s.begin() != s.end();
      • 使用迭代器遍历字符串/vector,确保不越界,利用递增运算符++
        for(auto it = s.begin(); it != s.end() && !isspace(*it) ;++it)(isspace检查是否为空白字符)
      • 使用迭代器访问成员
        • 若迭代器所指对象恰好为类,则可以使用它访问类成员 (*it).empty括号不可少,表示先对it解引用再执行点运算符
          同时也可以使用箭头->,it->men(*it).men相同
          • 因此,上遍历字符串也可以写成for(auto it = s.begin(); it != s.end() && !it->empty() ;++it)
  • 数组
    • 数组与vector的不同:数组大小确定不变,不能随意增加元素
    • 数组的初始化:
      • 数组初始化,维度一定是一个常量表达式(部分编译器支持使用变量定义,但最好还是使用常量),定义数组不能使用auto关键字,数组的元素是对象,不存在引用的数组
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        int arr[10];//一般初始化
        //显式初始化,列表初始化,可以指定部分值(<=维度),也可以指定全部值
        const unsigned sz = 3;
        int ial [sz] = {0, 1, 2};//可以指定元素
        int a2 [] = {0, 1, 2};//可以不指定元素个数显式初始化
        string a4[3] = {"hi", "bye"};//与a4[3]= {"hi", "bye", ""}相同
        //字符数组:额外的初始化形式——使用字符串字面值对数组进行初始化
        char a1[] = {'c', '+', '+'};//可以,但没有必要,最好加上空字符
        char a1[] = {'c', '+', '+', '\0'};//结尾一定要有空字符作为结尾
        char a2[] = "C++";//维度自动加上了表示字符串结束的空字符
        //若要定义上述以字符串字面值定义的C-字符串的维度,需要保证维度包含空字符的长度
        字符串赋值不允许拷贝,也不允许字符串之间相互赋值
      • 复杂数组的声明【指针数组与数组指针】:可以定义存放指针的数组,也可以定义数组的引用,但是不存在引用数组(以引用组成的数)
        • int *ptra[10];//含有十个整型指针,指针数组
          int (*p)[10] = &arr;//p指向一个含有10个整数的数组
          int (&arrref)[10] = arr;//arrref引用一个含有十个整数的数组
          int *(&array)[10] = ptra;//array是数组的引用,array的引用对象是一个大小为10的指针数组
        • 理解:改变阅读顺序 【数组指针与指针数组】
          • 指针数组:对于ptra,数组名紧贴了数组维度和*符号,类型修饰符从右向左依次绑定,因此对于行1代码,首先知道它是一个大小为10的数组,名字为ptra,数组中存放指向int的指针
            • 将指针数组传参给函数时,需要传递一个二级指针(指向指针的指针),同时还要另外以int传入数组的大小
          • 数组指针:对于p来说,数组维度紧跟被声明的名字,应该先由内而外读,*p表示p是一个指针,再从右往左读,看右,指针指向的是一个大小为10的数组,再看左,它指向的数组中元素的类型是int
            • 将二维数组传参给函数的时候,要使用数组指针(例如,char (*strs)[NUM]char strs[][NUM]),同时还要另外以int传入数组的大小
          • 数组指针指向的数组大小可知,但是指针数组里的指针指向的对象大小未知。
          • C-字符串的两种储存方式:
    • 访问、修改数组元素(对于一般数组而言)
      • 可以使用范围for,也可以使用范围for
      • 可以通过下标修改数组元素
    • 指针和数组
      • 关于指针的运算符:引用(&)与解引用(*);
        以及访问数组元素的方括号运算符([]) 【数组指针与指针数组】
        • 可以使用取地址符’&'获取某个对象的地址,赋给某个指针,例如string *p = &num[0]p指向nums的第一个元素
        • 解引用运算符(*):获取地址中储存的值(即获取目标的内容)
        • (个人理解)当指针指向一个数组时,就如数组名可以当作首元素地址来用一样,指向该数组首元素(非首元素也可以)的指针也可以当作首元素地址来用,那么它也可以使用下标运算符,像数组一样进行操作
        • 参考书中内容(c++程序设计基础教程p124)
          • image.png
          • image.png
        • 注意:对于一个重载了[]运算符的类对象来说,它的&s与&s[0]是不同的,前者表示该对象的地址,后者则表示s[0]返回的对象的地址!!!!
      • 数组与指针存在联系:string *p2 = nums;等价于p2 = &nums[0],*p2 = nums[0],注意此处解引用符号,在定义一个指针的时候,符号只是代表指针,而指针作为左值时使用符号则表示解引用,直接读取了p2指针指向的地址储存的内容
      • 数组下标和指针
        • 数组下标是一种size_t类型,是一种无符号类型
        • 指针运算
          • 给指针(指针名)加上、减去一个整数【这里的整数也可以是一个整型常量】,结果仍然是指针,新指针指向元素与原来指针相比前进、后退了;int *ip2 = ip +4;,ip2等于ip指针前进了四个位置后得到的地址
            注意不可越界
          • 两个指针相减的结果是它们之间的距离,但两个指针必须指向同一个数组中的元素
            两个指针相减的结果类型为ptrdiff_t,是一种带带符号类型
          • 指针运算适用于空指针,与所指对象并非数组的指针
        • 下标与指针
          • 数组名其实是一个指向数组首元素的指针,也就意味着,只要某指针与数组(首元素)指针相同,那么它就可以指向该数组中的全部元素,也可以使用下标形式表示数组
            1
            2
            3
            int ia[] = {0, 2, 4, 6, 8};
            int *p = &ia[2];
            int k = p[-2];//p[-2]等价于ia[0],类似地,p[1]等价于*(p+1),等价于ia[3]

            与string和vector不同,数组的下标运算符可以处理负值
          • 若指针指向了一个元素,允许解引用该结果指针来表示相应的元素
            若ia是一个数组;*(ia+4)等于ia[4]
      • 指向数组元素的指针也是迭代器,指针作为迭代器使用
        • 对于尾后指针的获取可以设法获取一个数组尾元素之后不存在的元素地址,让下标越界1即可int *e = &arr[10];(假设arr只有九个元素),注意此时的尾后指针不能进行解引用(*)或递增操作
        • 标准库函数beginend【不是成员函数,头文件
          代替上条的越界指定的形式,以int *last = end(ia);代替上条中代码,同样地,尾后指针不能进行解引用与递增操作
    • C风格字符串——尽量不要在C++中使用之
      • 可用于操作C风格字符串的string库函数 C-字符串的操作函数
      • C-字符串的两种储存方式:
        • 使用字符数组来储存:将字符串中的每一个字符数据储存在字符数组中,并追加一个’\0‘表示储存结束char name[] = "jack";
        • 使用字符指针来储存字符串数据,直接将字符串数据初始化给一个字符指针,指针表示首地址指向的字符char *name = "jack";
        • C-字符串的储存位置:
          • 字符数组储存的C-字符串:
            • 当被定义为局部自动数组时,它们在栈空间
            • 当被定义成全局数组、静态全局数组或静态局部数组,则储存在全局数据区常量池中
          • 以指针数组的方式储存:
            • 当被定义为局部自动变量时,指针存放在栈空间,字符串以字符数组的形式存放在全局数据域常量池中,此时各字符串的长度不受限制,且长度不一定相同。
            • 当被定义为全部变量时,指针变量储存在常量区,字符串数据以字符数组形式储存在常量区
          • 另一种解释方法
            • 当二者都是局部自动变量时:
              • 字符数组申请在栈区,字符串每个字符储存在这个字符数组的每一个元素中;指针变量声明在栈区,字符串数据以字符数组的形式储存在常量区
            • 当它们都作为全局变量时:
              • 字符数组储存在常量区(全局数据区),指针变量储存在常量区,字符串数据以字符数组形式储存在常量区
        • 关于修改值:
          • 以字符数组形式储存字符串数组,不管是全局还是局部,均可以使用下标修改字符数组中元素
          • 以字符指针形式储存的字符串数组,不论全局还是局部,不可以使用指针修改指向字符的数据,只能通过改变指针指向的值来修改数据
          • 当我们以字符指针的形式要将字符串数据存储到常量区的时候,并不是直接将字符串存储到常量区,而是先检查常量区中是否有相同内容的字符串,如果有直接将这个字符串的地址拿过来返回,如果没有,才会将这个字符串数据存储在常量区中
          • 当我们重新为字符指针初始化一个字符串的时候,并不是修改原来的字符串,而是重新的创建了一个字符串,把这个新的字符串的地址赋值给它
        • 参考[C]字符串数据在C中的存储方式
    • 多维数组(数组的数组)
      • 第一维表示数组本身的大小,第二维表示其元素的大小;阅读时按照从左至右的顺序阅读;对于二维数组,第一维叫做行,第二维叫做列
      • 多维数组初始化
        • 列表初始化
          • 使用花括号括起来表示多维中的一维(对于初始化元素的维度有明确指定)
          • 不使用花括号,则依次填入元素(对元素的维度不指定,只是顺序放入)
      • 多维数组下标的引用
        • 若下标运算符的数量和数组维度一样多,则表示相应的元素
        • 若下标运算符的数量小于数组维度,则表达式的结果是其一个内层数组(即第n个一维数组的首元素)
      • 使用范围for处理多维数组 书p114 未定
      • 指针与多维数组
        • 多维数组的名字也可以作为指向数组首元素的指针使用
        • 多维数组的指针实际上是一个指向多维数组内层数组的指针
          1
          2
          3
          4
          int ia[3][4];//由三个四维数组组成
          int (*p)[4] = ia;//p指向含有四个整数的数组
          p = &ia[2];//p指向ia尾元素;
          p = ia+3;//或使用指针
        • (C++11)使用auto类型,声明auto p = ia;,代替int (*p)[4] = ia;
        • 使用类型别名简化多维数组的指针 未定
    • 动态数组 未定
      • 申请堆变量:new 数据类型;释放堆变量:delete 指向堆地址的指针变量;
      • 申请堆数组:new 数据类型[元素个数];delete [] 指向堆地址的指针变量;
        • 申请不成功时,new操作将会返回NULL;由于计算机的可用空闲内存单元(堆区)可能形成大量的碎片单元,因此小规模字节数连续单元的申请容易成功;而大规模字节数连续单元申请时,失败的可能性高
        • 堆数组的申请有两种思路
          • 1.申请连续的地址
          • 2.申请不连续的地址,通过指针数组创立联系
      • 遵守一个delete原则:先申请的最后释放
      • delete后,指针的指向不变,只是将内存还给系统(将该处内存改为“空闲”状态),程序中一般需要约定:负责指向堆内存单元的指针,如果非空则表明申请成功(亦称指针带堆内存资源);如果释放了就应该将指针置成空指针,表明指针不带资源。

1.4表达式

  • 基本概念
    • 作用于一个对象:一元运算符;作用于两个运算对象:二元运算符;作用于三个运算对象:三元运算符;函数也是特殊的运算符,它堆运算对象的数量无限制
    • 左值与右值:当对象被用作右值的时候,引用的是对象的值(即内容,或许可以当作字面值理解),当对象被用作左值的时候,用的是对象的身份(即在内存中的位置,或许可以当作可以继续被赋值的意思)
      • 若无特例( 未定 ),需要右值的地方可以使用左值来代替,但是不能把右值(内容)当成左值(位置)使用,而左值被当成右值使用时,实际使用的是它的内容。
  • 优先级与结合律(可以在拿不准的时候使用括号)
    • C++运算符优先级别表p147
    • 四种规定了运算对象的求值顺序:逻辑与(&&)先求左侧运算对象,只有左侧为真才会继续秀右侧的值,逻辑或(||),条件(?:),逗号(,)
  • 算数运算符
    • image.png
    • 一元正号、加减法运算符均可以作用于指针
    • 当作用于算术类型的对象时,整数相除的对象还是整数,商的小数部分会被直接弃除,(C++11规定,负值的商也不能向上或向下取整,一律切断小数部分)
    • 对于取余运算:若m、n是整数且n非0,则(m/n)*n+m%n的值与m相等,即若m%n不等于0,则它的符号与m相同。除了(-m)导致溢出的情况,(-m)/n和m/(-n)的值与-(m/n),m%(-n)等于m%n,(-m)%n等于-(m%n)
  • 逻辑和关系运算符
    • image.png
      • 短路求值:先求左侧再求右侧,左侧无法满足对象是才会进行右侧求值
        • 对于逻辑与,当且仅当左侧为真时才会进行右侧求值
        • 对于逻辑或,当且仅当左侧为假时才会进行右侧求值
      • 关系运算符满足左结合律, 不可以使用连等
  • 赋值运算符=
    • 赋值运算符的左侧运算对象必须是一个可修改的左值
    • 赋值运算符满足右结合律,即先结合右侧值,再赋给左侧值
    • 赋值运算的优先级较低,低于逻辑运算,因此需要加上括号
    • 注意相等运算符==与赋值运算符=不同
    • 复合赋值运算符:
      • 算术运算符 += -= *= /= &= 位运算符 <<= >>= &= ^= !=
      • 任意一种都等价于a = a op b;但是二者求值次数不同,复合运算符只需求值一次,而普通运算符需要求值两次
    • 赋值表达式的结果为运算符左侧的变量本身
  • 递增和递减运算符(优先级高于解引用)++ –
    • 前置【一般都用前置递增/减运算符】:先对对象进行加减操作,再将改变后的对象作为求值结果
    • 后置【不推荐使用】:求值结果是运算对象改变之前的值的副本;
      使用场景:在一条复合表达式中既对其值加减1,又能使用它原来的值,此时推荐使用后置运算符
      • 例如,
        1
        2
        3
        4
        5
        cout<<*p;
        p++;
        //与下语句等价
        cout<<*p++;
        //由于解引用优先级较低,与*(p++)等价,p先将原值作用于解引用,再进行指针得移动操作
    • 注意未定义行为的发生,即赋值运算左右两端使用了同一变量,且该变量的值在右侧运算对象中还被改变 未定
  • 成员访问运算符 . ->
    • 用于访问成员,其中点运算符作用于非指针对象,箭头作用于指针
      假设ptr是一个指针ptr->mem(*ptr).mem左右相同
    • 注意:解引用运算符优先级低于点运算符,因此必须加上括号
  • 条件运算符 ?:
    • cond ? expr1:expr2
      cond:判断条件的表达式 expr1,2是两个类型相同或可能转换为某个巩固类型的表达式;条件运算符先求cond的值,若条件真对expr1求值并返回该结果的值,若条件为假则对expr2求值并返回该值。
    • 当条件运算符的两个表达式都是左值或者都能转换哼同一种做左值类型时,运算结果是左值,否则是右值
    • 嵌套条件运算符
      • 在条件运算符内部嵌套另外一个条件运算符,即if() {…}else if(){…}
    • 条件运算符在输出表达式中的使用
      • 条件运算符优先级较低,需要在它两端加上括号,若不加括号,可能变成对cout进行比较。
  • 位运算符
    • image.png
    • 位运算符作用于整数类型运算对象,把运算对象看作二进制位的几何,位运算符提供检查和设置二进制位的功能。
    • 建议将位运算符处理无符号类型,对于负数,处理情况是未定义行为
    • 移位运算符 >> <<
      • 1.对其运算对象执行基于二进制位的移动操作,首先令左侧运算对象的内容按照右侧运算对象的要求移动指定位数,然后将经过移动的左侧运算对象拷贝作为求值结果。
        2.右侧运算对象一定不能为负,值必须严格小于结果的位数,否则会产生未定义行为。
        3.移出边界外的位直接被舍弃。
        4.若对象是“小整型”,则它的值会被自动提升
        • 例子: image.png
      • 满足左结合律,即从左往右依次进行。
      • 位移运算符的优先级比算术运算符优先级低,但是比关系运算符、赋值运算符、条件运算符的优先级高
    • 位移求反符~
      • 将运算对象逐位求反后生成一个新值,将1置为0,将0置为1,注意若有类型的改变,则在改变后再进行求反
    • 位与、位或、位异或运算符 & | ^
      • 在两个运算对象上逐位执行相应的逻辑操作,然后生成相应的结果
      • 位与:如果两个运算对象的对应位置都是1,则运算结果中该位为1
      • 位或:如果两个运算对象的对应位置至少有一个为1,则运算结果中该位为1
      • 位异或:如果两个运算对象的对应位置有且只有一个为1,则运算结果为1
    • 位运算符的使用场景
      • 可以用于记录状态(1,0两种状态),但是此时要注意设置的数据类型的最小位数,以免出现位数不够的情况
  • sizeof运算符
    • 返回一条表达式或一个类型名字所占的字节数,满足右结合律,所得的值是一个size_t类型的常量表达式
    • 两种形式:
      • sizeof (type)
      • sizeof expr返回表达式结果类型的大小
    • sizeof不实际运算其运算对象的值,因此在sizeof中解引用无效指针仍然是安全的行为
    • sizeof运算符的结果依赖于其作用的类型
      • 对char或类型为char的表达式执行sizeof运算,结果为1
      • 对引用类型执行sizeof则会得到被引用对象所占的空间大小
      • 对指针执行sizeof运算则会得到指针本身所占的空间大小
      • 对解引用的指针使用,则会得到指针指向的对象所占的空间的大小
      • 对数组执行sizeof则得到整个数组所占的空间大小,等价于对数组中所有元素各进行一次sizeof并进行求和,sizeof不会将数组转换成指针处理
        • 可以用数组大小除以单个元素大小得到整个数组中元素个数,若sizeof的返回值是一个常量表达式,则sizeof结果可以用来声明数组维度
      • 对string对象或vector对象执行sizeof运算只返回固定部分的大小,不会计算对象中元素占用了多少空间
  • 逗号运算符
    • 求值顺序:首先对左侧的表达式求值,然后将求值的结果丢弃,逗号运算符真正的结果是右侧的表达式,若右侧运算对象是左值,则最终的求值结果也可能是左值。
    • 本质:将一系列语句顺序执行
  • 类型转换
    • 隐式转换:类型转换自动执行无需程序员介入
      • 隐式转换的发生情况
        • 大多数情况下比int类型小的整型会首先提升为较大的整数类型
        • 在条件中,非布尔值转换为布尔类型
        • 初始化过程中,初始值转换成变量的类型;赋值语句中,右侧运算对象转换成左侧运算对象的类型
        • 若算术运算或关系运算对象有多种类型,则需要转换成同一种类型
        • 函数调用时也会发生类型转换
    • 算术转换 未定
      • 将运算符的运算对象转换成最宽的类型
      • 整型提升
        • 负责把小整数类型转换成较大的整数类型,对于bool、char、signed char、unsigned char、short、unsigned short,只要所有可能指都能存在int中,便会提升至int类型,否则,提升成unsigned int类型
        • 较大的char类型(wchar_t、char16_t、char32_t)会提升至int、unsigned int、long、unsigned long、long long、unsigned long long中最小且可以转换后能容纳原类型所有可能值的类型。
      • 无符号类型的运算对象
        • 首先执行整型提升,如果结果类型匹配,则无需进行进一步转换,若提升后运算对象的类型要么都是带符号的,要么都是无符号的,则小类型的运算对象转换成较大类型
        • 如果一个运算对象是无符号类型、另一个是带符号类型,且无符号类型不小于带符号类型,则带符号的运算对象转换成无符号的。但如果此时带符号类型是负数,则将会发生转换值对最大值取模的情况。
        • 当带符号类型大于无符号类型时,此时转换的结果依赖于机器。若无符号类型的所有值都能存在该带符号该带符号类型中,则无符号类型的运算对象转换为有符号类型,如果不能,则带符号类型的运算对象转换成无符号类型。
      • 其他隐式类型转换
        • 数组转换成指针
          • 大多数数组表达式中,数组自动转换成指向数组首元素的指针
          • 当数组被当作decltype关键字的参数,或作为取地址符(&)、sizeof以及typeid等运算符的运算对象时,上述转换不会发生。
          • 如果用一个引用来初始化数组,上述转换也不会发生
          • 在表达式中使用函数类型时会发声类似的指针转换
        • 指针的转换
          • 常量整数值0或字面值nullptr可转换成任意指针类型,指向任意非常量的指针可以转换成void * ,指向任意对象的指针可以转换成const void*
          • 以及另外一中转换方式
        • 转换成布尔类型:指针或算术类型的值为0,转换为false,否则转换为true
        • 转换成常量:允许将指向非常量类型的指针转换成指向相应的常量类的指针,对于引用也如此。即可以将指向一种类型的指针或引用分别转换成指向常量的指针或引用(即将对象转换成底层const)。 顶层const与底层const(书p57)
        • 类类型定义的转换:由编译器自动执行,但每次只能执行一种类类型的转换 未定
    • 显式转换:
      • 命名的强制类型转换:
        • case-name<type>(expression)type:转换的目标类型,expression,要转换的值。若type是引用类型,则结果是左值。cast-name是static_cast、dynamic_cast、const_cast、reinterpret_cast中的一种。 未定
        • static_cast
          • 任何具有明确定义的类型转换,只要不包含底层const,都可以使用它。当需要把一个较大的算术类型赋值给较小的类型时可以使用,此时编译器会忽略精度损失的警告信息。
          • 可以使用static_cast找回存在于void*指针中的值,但是需要注意,使用static_cast转换时应该确保转换后的结果与原地址值相等,必须确保站换后的类型就是指针所指的类型,一旦类型不符,间产生未定义后果
        • const_cast
          • 只能改变运算对象的底层const,可以去掉const性质,编译器便不再阻止对对象的读写操作。若对象本身不是常量,则强制类型转换获得写权限是合法行为,若对象是一个常量,再使用此执行操作就会产生未定义后果。
          • const_cast只能改变表达式的常量属性,不能在<>中使用其他形式,改变表达式类型。
          • 使用场景:有函数重载的上下文中
        • reinterpret_cast 十分危险,可能导致异常运行行为 未定
      • 旧强制类型转换
        • type (expr); (type)expr;
        • 使用旧强制转换时,如果换成const、static也合法,则进行对应的转换
        • 替换后不合法时,旧强制转换类型执行与
  • 运算符优先级表
    • image.png
    • image.png

1.5语句

  • 简单语句(略)
    • 对于一个连续的比较语句,如'0' <= str[i]<= '9',编译器不会进行连续比较的处理,而是从左向右一次进行比较先判断是否>=‘0’,结果无非0或1,之后的结果一定<=‘9’,因此此句变成了永真的一句表达式
  • 复合语句(块):花括号括起来的语句和声明序列;块不以分号结束
  • 条件语句
    • if语句
      • if中的condition都需要用圆括号包围起来 ,condition里可以是一个表达式,也可以是一个初始化了的变量声明,不管时表达式还是变量,都必须可以转换成bool类型。
      • 注意,花括号的使用
    • switch语句
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      switch(expression){
      case constant-expression :
      statement(s);
      break; // 可选的
      case constant-expression :
      statement(s);
      break; // 可选的

      // 您可以有任意数量的 case 语句
      default : // 可选的
      statement(s);
      }

      • switch语句首先对括号里的表达式求值,表达式紧跟在关键字switch后,可以时一个初始化的变量声明,表达式的值之后被转换成整数类型,与每个case标签的值进行比较
      • 如果表达式和某直到知道达到了switch结尾或是遇到一条break语句位置;如果switch语句的表达式和所有case都没有匹配上,将直接跳转到switch 结构之后的第一条语句。
      • case标签必须是整型常量表达式,任何两个case标签的值不能相同,否则会引发错误
      • switch内部控制流
        • 多个值共享一个操作,可以故意省略break语句
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          12
          13
          switch(expression){
          case constant-expression1 ://也可以不换行
          case constant-expression2 :
          case constant-expression3 :
          //可以有更多语句
          case constant-expressionn :
          statement(s);
          break; // 可选的

          // 您可以有任意数量的 case 语句
          default : // 可选的
          statement(s);
          }
        • 执行每个case后的break最好不要省略,如果省略,该匹配case后的所有case的语句都会被执行一遍
        • default标签
          • 如果没有任何一个case标签能匹配switch表达式的值,程序将执行紧跟在default后的语句,最好加上default语句,若无需执行,则只要跟上空语句或空块即可
        • 对于switch内部的变量定义:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处是非法行为。不允许跨国变量的初始化语句直接跳转到该变量作用域内的另一个位置。
  • 迭代语句
    • while语句:不确定到底迭代多少次时,使用while循环
    • 传统for语句
      1
      2
      3
      4
      for ( init; condition; increment )
      {
      statement(s);
      }
      • init可以、且只能是:声明语句、表达式语句或空语句,这些语句都要以分号作为结束;for语句头定义的对象只在for循环体内可见,还可以定义多个对象,但是这些对象的类型都要相同
      • initcondition,_increment_语句都可以省略,但要注意前两者不能省略分号,若省略condition,则相当于永真语句
    • 范围for
      • 使用范围for对每一个字符进行处理(C++11)
    • do while 语句
      • 与while语句十分相似,不用之处在于,do while先执行语句再检查条件,无论条件的值如何,至少执行依次循环
        1
        2
        3
        4
        5
        do
        {
        statement(s);

        }while( condition );//这里有一个分号,表示语句结束
      • do while语句不允许再判断条件部分定义变量
  • 跳转语句
    • break语句
      • 中止离它最近的while,dowhile,for或switch语句,并且从这些语句之后的第一条语句开始执行。
      • break语句只能出现在迭代语句或者switch语句内部,break语句的作用范围仅限最近的循环或switch
    • continue语句
      • 中止最近循环中的当前迭代,并且立即开始下一次迭代,只能出现在for,while,dowhile循环内部,只有switch嵌套在循环语句内部时才可以使用continue
    • goto语句*不要使用
  • try语句块和异常处理
    • throw表达式:异常检测部分使用throw表达式表示它遇到了无法处理的问题。throw引发了异常
      • throw表达式可以替代输出异常的句子,直接抛出一个异常throw runtime_error("Data must refer to same ISBN")
    • 异常类:用于在throw表达式和相关的catch子句之间传递异常的具体信息
    • try语句块:以try关键字开始,并以一个或多个catch子句结束,try语句块中的代码抛出的异常通常会被某个catch子句处理。这种语句也被称为异常处理代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      try
      {
      progaram-stations// 保护代码
      }catch( ExceptionName e1 )
      {
      handler-stations// catch 块
      }catch( ExceptionName e2 )
      {
      handler-stations// catch 块
      }catch( ExceptionName eN )
      {
      handler-stations// catch 块
      }
      • progaram-stations组成程序的正常逻辑,像其它任何块一样,可以包含声明在内的任意C++语句,try语句块内声明的变量在块外部无法访问,在catch子句内也无法访问。一般try后代码块会抛出异常,之后由catch捕捉异常,进行相应处理。
      • 由于try语句块可能调用了包含另一个try语句块的函数,新的try语句块可能调用了包含新的try语句块的函数,因而在复杂系统中,程序在碰到异常代码前可能已经经过了多个try语句块。寻找处理代码的过程与函数调用链刚好相反,当异常被抛出时,首先搜索抛出异常的函数,如果没有找到匹配的catch子句,则终止该函数,并且在调用那个该函数的函数中继续寻找,若还是没有找到匹配的catch子句,则这个新的函数也被终止,继续搜索调用它的函数,以此类推,直到找到适当类型的catch子句为止。
        如果最终没有找到匹配的catch子句,则程序转到名为terminate的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。
        如果没有try语句块定义异常,则按照没有找到catch语句的方法进行处理,系统会调用terminate函数,并终止当前程序的运行。
    • 标准异常:C++标准库定义了一组类,用于报告标准库函数遇到的问题,这些异常类也可以用在用户编写的程序中。
      • exception头文件定义了最通用的异常类exception,它只报告异常的发生,不提供任何额外信息
      • stdexcpet头为念定义了集中常用的异常类
        • image.png
      • new头文件定义了bad_alloc异常类型,在书p407页介绍
      • type_info头文件定义了bad_cast异常类型,在p731介绍
        标准库异常类只定义了几种运算,包括创建或拷贝异常类型的对象,以及为异常类型对象赋值。我们只能以默认初始化【在定义变量时如果没有为其指定初始化值,则该变量会被C++编译器赋予默认的值】的方式初始化exception,bad_alloc和bad_cast对象,不允许为这些对象提供初值。
      • 对于其它类型的行为则应该使用string对象或者C风格字符串初始化这些类型的对象,但不允许使用默认初始化的放射,创建此类型对象时必须提供初始值,该初始值含有错误相关的信息。
      • 异常类型只定义了一个名为what的成员函数,该函数没有任何参数,返回值是一个指向C风格字符串的const char*,该字符串的目的是提供给关于异常的文本信息。如果异常对象的类型有一个字符串初值,则what返回该字符串,对于其它无初始值的异常类型,what返回的内容由编译器决定。

1.6函数

  • 1.函数基础
    • 函数包括:返回类型、函数名字、由0个或多个形参组成的列表、函数体组成
    • 通过调用运算符执行函数,调用运算符的形式是一对圆括号,作用于一个表达式,该表达式时函数或者指向函数的指针,圆括号内时一个用逗号隔开的实参列表,用实参初始化函数的形参,调用表达式的类型就是函数返回的类型
    • 形参和实参
      • 实参时形参的初始值,实参与形参存在对应关系,但是没有规定实参的求值顺序。
      • 实参的类型必须与形参匹配,函数形参的储存类型为auto,其生命周期存在于函数被调用之时
      • 函数(声明中的)形参列表中的形参通常用逗号隔开,每个形参都是含有一个声名符的声明。即使类型相同也必须声明。
      • 形参必须不可同名,即使是没有使用的形参,也必须为它提供一个实参
    • 函数返回类型
      • 可以是void,表示函数不返回任何值,函数返回类型不能是数组类型或函数类型,但是可以是指向数组或函数的指针。
    • 局部对象
      • C++中,名字有作用域,对象有生命周期。名字的作用域是程序文本的一部分,名字在其中可见;对象的生命周期是执行过程中该对象存在的与段时间
      • 局部变量:形参和函数体内部定义的变量,仅在函数作用域可见。局部变量会隐藏在外层作用域中同名的其他所有声明中。局部变量的生命周期依赖于定义的方式。
      • 自动对象(auto):只存在于块执行期间的对象。形参就是一种自动对象。
        • 自动对象的表现:块执行结束后,块中创建的自动对象便成为未定义的(即销毁)。对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当达到定义所在的块末尾时销毁它。
          传递给函数的实参会被用于初始化形参对应的自动对象。
          对于局部变量对应的自动对象来说 ,若变量定义本身含有初始值,就用这个初始值进行初始化,否则若变量本身不含有初始值,则执行默认初始化。这意味着内置类型[内置类型分为三个主要类别:整型、浮点和void]的未初始化局部变量将产生未定义的值。
      • 局部静态对象:static类型的对象,用于将某个局部变量的生命周期贯穿函数调用以及之后的时间。
        • 局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行对它也没有影响。
        • 如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。
    • 函数声明(函数原型)
      • 函数可以定义一次,但是可以声明多次,如果一个函数永远也不会被用到,那么它可以只声明不定义。
      • 函数声明无需函数体,用一个分号代替即可,函数声明无需函数体,因此也无需形参的名字,但是写上形参名更有利于使用者理解函数功能。
      • 函数声明也称作函数原型。
      • 建议函数在头文件声明,在源文件定义。把函数声明放在头文件中可以确保同一函数的所有声明都保持一致,而一旦我们想改变函数的接口,只需改变一条声明即可。定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
    • 分离式编译 未定 需要练习
      • 分离式编译可以把程序分隔到几个文件中去,每个文件独立编译。
  • 2.参数传递
    • 在函数原型中(即函数声明中),函数的形参名可以省略,但是在函数定义里不能省略形参名

    • 形参初始化机理与变量初始化一样,函数形参的储存类型为auto,其生命周期存在于函数被调用之时
    • 形参的类型决定了形参与实参的交互方式,若是引用型实参,它则绑定到对应实参上,否则实参的值拷贝后赋给形参。
      • 引用传递、引用调用:形参是引用类型或函数被传引用调用
      • 值传递、传值调用:实参的值被拷贝给形参,实参和形参是独立对象,可以说实参被值传递,或者函数被传值调用。
    • 传值参数
      • 指针形参:指针的行为和其它非引用类型一样,执行指针拷贝时,拷贝的是指针的值,拷贝后两个指针时不同的指针,因为指针使我们可以间接地访问它所指的对象,因此通过指针可以修改它所指对象的值。
      • 建议使用引用形参代替指针来完成引用传递的效果。
    • 传引用参数
      • 通过引用形参允许函数改变一个或多个实参的值
      • 引用形参作用
        • 避免拷贝:避免对于大的类类型对象或者容器对象的低效拷贝,引用还可以解决有的类类型不支持拷贝操作的情况,若不想更改参数的内容,可以把形参定义成对常量的引用。(如,string对象可能会很长,应该尽量避免直接拷贝它们)
        • 返回额外信息:函数只能返回一个值,但是有时函数需要同时返回多个值,可以定义一种新的数据类型类解决,也可以通过给函数一个额外的引用参数来解决。
      • 传递指针的引用:既可以改变指针目标处的内容(解引用),也可以改变实参指针本身的指向(通过引用指针实参改变其地址的值)
    • const形参和实参
      • 使用实参初始化形参时会忽略形参的顶层const,当形参有顶层const时,传给它常量对象或是非常量对象都是可以的。
      • C++中,可以允许定义若干具有相同名字的函数,但是前提是函数的形参列表应该不同,倘若使用了有顶层const的形参,则若定义了名字相同但无无顶层const的形参,将会被视作重复定义。如void fcn(const int i){}void fcn (int i ){}两个函数会被视作重复定义
      • 指针或引用形参与const
        • 形参的初始化方式与变量初始化方式一样,可以使用非常量初始化一个底层const对象,但是反之不行,(若一个函数的参数为普通引用型 int &)普通的引用必须使用同类型的对象初始化,不能使用字面值、求值结果为int的表达式、需要转换的对象或者const int类型的对象;类似地,若函数的参数是int*,则只能传入int*实参,即只能传入一个地址。
          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          11
          reset (int &i);//需要传入指向i的指针
          reset(int *ip);//需要传入地址(be like 指针初始化需要地址)
          int i = 0;
          const int ci = i;
          string ctr = 0;
          reset(&i);//调用了形参类型是int*的函数,传入了一个地址
          reset(&ci);//错误:不能用指向const int对象的指针初始化int*
          reset(i);//调用reset(int &i)
          reset(ci);//错误:不能把普通引用绑定到const对象上
          reset(42);//错误:普通引用不能绑定到字面值上
          reset(ctr);//错误:类型不匹配
        • 尽量使用常量引用:
          • 第一,防止不必要的数据更改
          • 第二,使用引用而非常量引用会极大地限制函数所能接受的实参类型,const对象、字面值或者需要类型转换的对象都不能作为实参传递给普通的引用参数
          • 第三,如果其他函数的形参是常量引用,那么在该函数内调用的函数若是普通引用,就不能直接使用这个引用形参而要再创建一个变量成为常量引用的副本,再传值。
    • 数组形参
      • 为函数传递数组时,实际上是传递了指向数组首元素的指针,可以把数组的形参写成类似数组的形式

        1
        2
        3
        4
        5
        6
        7
        //相同的形参,都是const int*
        void print (const int*);
        void print (const int[]);
        void print (const int[10]);//尽管维度表示我们期望数组含有多少元素,但是实际可能不一定
        int i = 0,j[2]={0,1};
        print (&i);//正确:传入int*类型
        prnit (j);//正确,j转换为指向j[0]的int*
      • 管理指针形参(防止越界的三种办法)

        • 使用标记指定数组长度
          • 要求数组本身包含一个结束标记,例如使用C-字符串,当遇到空字符的时候停止,适用于有明显结束标记且不会与普通数据混淆的情况,但是对于像int这样所有取值都是合法值得数据便不太有效。
        • 使用标准库规范
          • 传递指向数组首元素和尾后元素的指针,使用begin()函数和end()函数提供两指针
        • 显式传递一个表示数组大小的形参
          • 定义一个表示数组大小的形参,可以使用sizeof()计算,也可以使用begin()函数和end()函数得到两指针之后相减得到数组的大小
      • 数组形参和const

        • 当函数不需要对数组元素执行写操作时,数组的形参应该是指向const的指针
      • 数组引用形参

        • C++中允许将变量定义成数组的引用(p101),因此形参也可以时数组的引用,此时引用的形参绑定到对应实参上,也就是绑定到数组上

          1
          2
          void f (int &arr[10]);//错误,将arr声明为引用的数组
          void f(int (&arr)[10]);//正确:arr是具有十个数字的整型数组的引用
        • 使用数组引用形参时,如果规定了数组的维度,那么只能使用相应大小的数组,在p578中会介绍如何让它传递任意大小的数组

      • 传递多维数组

        • 多维数组实际上时数组的数组,那么在传递时就要传递一个数组指针,其中每一个指针指向的是每一个一维数组 【数组指针与指针数组】
          也可以如下例所示,传递一个最高维元素个数不指明,低维元素指明的(类似数组)的形参
          1
          2
          3
          4
          5
          int *matrix[10];//指针数组,由十个指针构成的数组
          int (*matrix)[10];//数组指针,指向含有十个整数的数组的指针
          int matrix[][10];//与上行等价
          void f(int matrix[][10]);
          void f(int (*matrix)[10]);//与上一行等价,形参的类型都是指向含有十个整数数组的指针
    • main:处理命令行选项 未定
      • 命令行选项会通过两个形参传递给main函数int main(int argc, char *argv[]){}也可以定义为int main(int argc, char **argv){}
        • 第一个形参argv表示数组中字符串的数量,第二个形参argc是一个数组,当实参传递给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参,最后一个指针之后的元素值保证为0
        • 示例:运用命令行指定程序运行的时候,需要提供输入的数据,可以通过命令行输入
          • 假如你的程序是hello.exe,如果在命令行运行该程序,(首先应该在命令行下用 cd 命令进入到 hello.exe 文件所在目录) 运行命令为:
          • hello.exe Shiqi Yu
            那么,argc的值是 3,argv[0]是"hello.exe",argv[1]是"Shiqi",argv[2]是"Yu"。
    • 含有可变形参的函数->编写可以处理不同数量实参的函数
      • 1.如果所有实参的类型相同且数量未知,可以传递一个名为initializer_list的标准库类型(C++11)
        • 可以使用initializer_list类型的形参,用于表示某种特定类型的值的数组
          • image.png
          • 与vector一样,initializer_list是一种模板类型,定义initializer_list对象时,必须说明列表中所含元素的类型,和vector不同的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。
          • 向initializer_list形参中传递一个值的序列,必须要将序列放入花括号内,例如func({"hello","world"})(当然也可以不是字符串)
      • 2.如果所有实参类型不同,可以编写一种特殊函数,即可变参数模板(p618)(C++11)
      • 3.使用特殊的形参类型(省略符)来传递可变数量的实参
        • 省略符形参是为了便于C程序访问某些特殊的C代码而设置的,这些代码使用了名为varags的C标准库功能,省略符形参应该仅仅用于C和C通用的类型,大多数类类型对象在传递给省略符形参时都无法正常拷贝。
        • 胜率符形参只能出现在形参列表的最后一个位置,它的形式只有一下两种
          • void foo(parm_list,...);指定了foo函数的部分形参的类型,对于这些形参的实参将会执行正常的类型检查,省略符形参所对应的实参无需类型检查,形参声明后面的逗号是可选的
          • void foo(...);
  • 3.返回类型和ruturn语句
    • 无返回值函数:
      • 返回void的函数,不要求非得有return语句,在这类函数后会隐式地执行return,如果想要在执行过程中退出可以使用return;强行返回其他类型参会则会产生编译错误
    • 有返回值的函数:
      • 只要函数的返回类型不是void,该函数内每条return语句必须返回一个值,return语句返回值类型必须与函数的返回类型相同,或者可以隐式地转换成函数的返回类型。
    • 值是如何被返回的:
      • 返回一个值的方式与初始化一个变量或形参地方式完全一样,返回的值用于初始化调用点地一个临时量,该临时量就是函数调用的将结果

      不要返回局部对象的引用或指针:

      - 函数完成后,其占用的空间便会被释放,因此局部变量的引用将指向不再有效的内存区域。类似地,返回局部对象的指针也是错误的。
      1
      2
      3
      4
      5
      6
      7
      8
      const string &manip()
      {
      string ret;
      if(!ret.empty())
      return ret;//错误:返回局部对象的引用
      else
      return "Empty";//错误:字面值"Empty"是一个局部临时string对象
      }
    • 返回类类型的函数和调用运算符
      • 调用运算符也有优先级和结合律,调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律,因此如果还能输返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员
    • 引用返回左值(返回变量)
      • 函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数得到左值,其它返回类型得到右值,可以像使用其它左值那样来使用返回引用的函数的调用,我们能为返回类型是非常量引用的函数的结果赋值
        • image.png
        • 由于引用时左值,因此可以像其它左值一样出现在赋值运算的左侧
        • 如果返回常量引用,则不能给调用结果赋值
    • 列表初始化返回值
      • C++11规定,函数可以返回花括号包围的值的列表。例如返回vector类型的对象,可以选择使用列表初始化,return {“hello”,“world"};或空列表return{};如果函数返回的时内置类型,则花括号包围的列表最多包含一个值,且该值所占的空间应该不大于目标类型的空间。若返回的类类型,则由类本身帝国一初始值如何使用。
    • main的返回值
      • 允许main函数没有return语句直接结束,如果控制达到了mian结尾而没有return语句,编译器将隐式地插入一条返回0的return语句
    • 递归:函数调用它自身
      • 递归函数中,一定有某条路径是不包含递归调用的,否则函数将永远递归下去,直到栈空间用完为止
    • 返回数组指针
      • 使用类型别名简化返回数组中的指针或引用的函数
        • image.png
      • 声明返回数组指针的函数:使用数组指针
        • image.png
          • 返回数组指针的函数type (*function(parameter_list))[dimension]例如int ( *func(int i) ) [10]
            • 其中function(parameter_list)是函数名+形参表
            • (*function(parameter_list))意味着我们可以对函数调用的结果执行解引用操作
            • (*function(parameter_list))[dimension]表示可以得到一个大小为dimension的数组,这里将函数与解引用符号括起来的括号十分有必要,如果没有,函数的返回类型将是指针的数组
            • type表示数组中的元素类型
        • 使用尾置返回类型
          • C++11中,任何函数的定义都可以使用尾置返回,这种方法对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或是数组的引用。尾置返回类型在形参列表后并以一个->符号开头,并且在本应出现返回类型的地方放置一个auto
          • auto func(int i) ->int(*)[10];
          • 这样做比较形象直观
        • 使用decltype关键字(C++11)
          • image.png
          • arrPtr函数使用关键字decltype表示它返回的类型是个指针,并且该指针所指的对象与odd一致,odd是一个含有5int元素的数组,因此arrptr指向同类型的数组的指针。由于decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,若要表示arrptr返回指针,则必须在函数声明中添加*
  • 4.函数重载:同一作用域内的几个函数名字相同但形参列表不同
    • 重载基础
      • main函数不能重载
      • 构成重载的情况:
        • 1.函数的形式参数个数不同
        • 2.形式参数个数相同时,参数数据类型不同,或数据类型出现次序不同
      • 不构成重载的情况:
        • 形式参数名字不同,或是仅仅函数的返回类型不同,不构成函数重载
        • 1.省略形参名字,形参类型相同
          • image.png
        • 2.形参的类型使用了别名
          • image.png
        • 3.重载和const形参
          • 顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来
            • image.png
            • const不能转换成其它类型,因此只能把const对象传递给const形参,而又因为非常量可以转换成const,因此以上四个函数都能作用于非常量对象或者指向非常量对象的指针,但是对于编译器来说,传入非常量时会优先使用非常量版本的函数
      • const_cast和重载
        • 可以运用重载函数将返回const引用的函数转为普通的引用
      • 调用重载的函数
        • 以合理的实参调用它们,把函数调用与一组重载函数中的某一个关联起来的过程叫做重载确定。
          • 匹配重载函数的规则 6.函数匹配
            • 1.找到一个数据类型严格匹配的函数
            • 2.通过数据类型的兼容性隐式转换寻找一个匹配
            • 3.通过用户定义的类型寻找一个匹配
        • 调用重载函数有三种可能的结果
          • 编译器找到了一个与实参最佳匹配的函数,并且生成调用该函数的代码
          • 找不到任何一个函数与调用的实际匹配,此时编译器发出误匹配的错误信息
          • 有多于一个函数可以匹配,但每个都不是明显的最佳选择,此时也不发生错误,被成为二义性调用
    • 重载与作用域
      • 在不同作用域中无法重载函数名,尽量不要在作用域中声明函数
  • 5.特殊用途语言特性(参数带默认值的函数、内联函数与constexpr函数)
    • 带默认值的函数
      • 默认实参:在函数的很多次调用中它们都被赋予了一个相同的值,调用包含默认实参的函数时,可以包含该实参,也可以省略该实参;一旦某个形参被赋予默认值,它后面的所有形参都必须有默认值
        • image.png
      • 使用默认实参调用函数:在调用函数时省略该参数即可
        • 函数调用时按其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右的实参),设计函数时,可以将不怎么使用 默认值的形参出现在前面,让那些经常使用默认值的形参出现在后面
          例如上函数,想要覆盖background的默认值,必须为ht和wid提供实参
          • image.png
            默认实参声明:给定作用域中的一个形参只能被赋予一次默认实参(建议默认值只出现在函数声明中,函数定义中不能再出现函数默认值),函数的后续声明只能为之前没有添加默认实参的形参添加默认实参,且右侧所有形参都要有默认值,且不能改变已经赋予的默认值;注意添加默认实参的格式
          • image.png
        • 默认实参初始值:
          • 局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参(可以是一个变量名)
            • image.png
    • 内联函数和constexpr函数
      • 内联函数可以避免函数调用的开销(缩短运行时间)
        • 此例:没有使用内联函数的shorterstring函数
          缺点:调用函数一般比求等价表达式要慢,因为一次函数调用包含一系列工作:保存寄存器,在返回时恢复,可能需要拷贝实参,程序转向一个新的位置继续执行
          image.png
        • 在原函数的返回类型前加上关键字inline就可以将其声明为内联函数
        • 内敛说明只是向编译器发出的请求(建议),编译器可以选择忽略这个请求(建议)
        • 内联机制可以用于优化规模较小、流程直接、频繁调用的函数,很多编译器不支持内联递归函数
          内联函数最好也不要含有复杂的控制流程语句或是递归函数
      • constexpr函数(返回constexpr的函数)
        • 能用于常量表达式的函数,定义constexpr函数的方法与其他函数类似,但是要遵循几项约定:函数的返回类型及所有形参的类型都是字面值类型且函数体中必须有且只有一条return语句
          • 例: image.png
        • constexpr函数可以初始化常量表达式
        • constexpr函数体内也可以包含其它语句,只要这些语句在运行时不执行任何操作即可,constexpr函数内部可以有空语句、类型别名以及using声明,允许返回一个非常量值,函数初始化
        • 允许constexpr函数返回值并非一个常量:
          • image.png
          • 如果用一个非常量表达式调用常量表达式函数,则返回值是一个非常量表达式,当把常量表达式函数用在需要常量表达式的上下文中时,由编译器负责检查函数的结果是否符合要求,如果恰好不是常量表达式,则编译器将会发出错误信息
      • 内联函数和constexpr函数通常放在头文件内定义
    • 调试帮助
      • 程序可以包含一些代码,但是代码只在开发程序时使用,当应用程序编写完成发布后,先屏蔽调试代码,一般用到两项与处理功能assert和NDEBUG
      • assert预处理宏
        • 预处理宏:是一个预处理变量,行为类似于内联函数
        • assert宏表达式:assert (expr);
          • 首先对expr求值,如果表达式为假,assert输入信息并中止程序的执行,如果表达式为真,assert什么也不做。
        • assert宏定义在cassert头文件中,预处理名字由预处理器而非编译器管理,因此可以直接使用预处理名字而无需使用using声明,即应该使用assert而不是使用std::assert
        • 宏名字在程序内必须唯一,含有ccassert头文件的程序不能再定义名为assert的变量、函数或其它实体,在实际编程过程中,即使没有包含cassert头文件,也最好不要使用assert
        • assert宏常用来检查”不能发生“的条件,如,一个对输入文本进行操作的程序可能
      • NDBUG预处理变量
        • assert行为依赖于NDEBUG的用于处理变量的状态,若定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG时,assert将执行运行时检查
        • 可以使用#define语句定义NDEBUG,从而关闭调试状态,也可以使用编译器中的一个命令行选项来定义预处理变量$ CC -D NDEBUG main.c #use /D with the Microsoft compiler ,两者效果相同
        • 定义NDEBUG可以避免检查各种条件所需运行时开销,可以将assert当作调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程序本身的错误检查
        • NDEBUG除了搭配assert使用,也可以代替assert的功能,如果NDEBUG没有被定义,则可执行#ifndef 与#endif之间的代码,如果定义了NDEBUG则这些代码将被忽略
        • 编译器定义的对程序调试的有用名字:
          1
          2
          3
          4
          5
          _ _func_ _ //输出当前调试函数的名字
          _ _FILE_ _//存放文件没的字符串字面值
          _ _LINE_ _//存放当前行号的整型字面值
          _ _TIME_ _//存放文件编译时间的字符串字面值
          _ _DATE_ _//存放文件编译日期的字符串字面值
          • image.png
  • 附:函数模板
    • 函数模板声明格式template <typename 形式数据类型列表> 返回类型 函数模板名(形参表){}函数模板只是描述了对某种假想的数据类型将要进行的操作,编译器不会编译函数模板本身,因此模板是声明而非定义 编译系统遇到具体的实际参数调用函数模板时,用实际参数的数据类型取代形式参数的数据类型,自动地为成员定义一个模板函数,也称为函数模板的实例化
    • 模板类型参数
      • 类型参数前必须使用关键字class或typename
        • 关键字可以同时使用,如<typename T,class U><typename T,U相同
      • 模板参数列表(类比函数形式参数列表)在使用模板时,隐式地指定模板实参,将其绑定到模板参数上
    • 重载函数模板:
      • 若重载了函数模板,则编译器会先匹配重载模板函数
    • 非类型参数
      • 作用:为模板中提供了一个常量参数;需要常量表达式的地方可使用
      • 注意:绑定到非类型整型参数的实参把必须是一个常量表达式,绑定到指针或引用非类型参数的实参必须有静态生存期。不能用普通的局部变量或动态对象作为指针或引用非类型模板的参数
      • 例子
        • image.png
  • 6.函数匹配
    • 确定候选函数和可行函数
      • 函数匹配的步骤:
        • 1.确定候选函数:重载函数集中的函数,具有两大特征,一是与被调用函数重名,二是声明再调用点可见
        • 2.考察本次调用提供的实参,然后从候选函数中选出可以被这组实参调用的函数,新选出的函数叫做可行函数,可行函数具有两个特征,一是形参数量与本次调用提供的实参数量相等(注意,如果函数有默认实参可以少于实参数),二是每个实参的类型对应的形参类型相同,或可以转换成形参的类型
        • 3.从可行函数里找到与本次调用最佳匹配的函数
      • 寻找最佳匹配
        • 逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。最匹配意味着实参类型与形参类型最接近
      • 含有多个形参的函数匹配
        • 当实参的数量有更多时,最佳匹配取决于以下条件:
          • 1.该函数每个实参的匹配都不劣于其它可行函数需要的匹配
          • 2.至少有一个实参的匹配优于其它可行函数提供的匹配
          • 如果检查所有实参后没有任何一个函数脱颖而出,则该函数调用是错误的,编译器将报告二义性调用的信息
    • 实参类型转换
      • 编译器将实参类型到形参类型的转换划分为几个等级,排序如下
        • 1.精确匹配
          • 实参类型和形参类型相同
          • 实参从数组类型或函数类型转换成对应指针类型
          • 向实参添加顶层const或从实参中删除顶层const
        • 2.通过const转换实现的匹配
        • 3.通过类型提升实现的匹配
        • 4.通过算术类型转换或指针转换实现的匹配
        • 5.通过类类型转换实现的匹配
      • 函数匹配和const实参
        • 如果重载函数的区别在于它们引用类型的形参是否引用了const或指针类型的形参是否指向const,则当调用发生的时候,编译器通过实参是否是常熟来决定选择哪个函数
        • 一般如果传入值是常量,则会调用引用const的函数,如果传入非常量对象,则会调用非常量版本的引用;指针类似,若实参是指向常量的指针,则调用const*的函数,如果指向非常量,则调用普通指针的函数
  • 7.函数指针
    • 函数的类型:由返回类型和形参类型共同决定,与函数名无关、
    • 声明函数指针:
      1
      2
      bool legthcompare (const string &, const string &);
      bool (*pf) (const string &, const string &);

      从声明的名字开始观察:pf前有一个*,表示pf是一个指针,右侧是形参列表,表示pf指向的是函数,再看左侧,函数返回的类型是布尔值
      • 注意:*pf两端括号不可忽略,如果不写这对括号,则pf是一个返回值为bool指针的函数,与愿意不符合
    • 使用函数指针
      • 将函数名作为一个值使用时,该函数自动地转换成指针,因此可以以如下方式将地址赋给指针pf
        • image.png
      • 也可以直接使用指向函数的指针调用该函数,无需提前解引用指针
        • image.png
      • 在指向不同类型的指针间不存在转换规则,但可以为函数指针赋一个nullptr或一个值为0的常量表达式,表示该指针没有指向任何一个函数
    • 重载函数的指针
      • 定义重载函数的指针必须与重载函数中的某一个精确匹配
    • 函数指针形参
      • 和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时形参看起来是函数类型,实际上却是当成函数指针使用,可以直接把函数作为实参引用,此时它会自动转换成指针
        • image.png
      • 也可以使用类型别名(如typedef)和decltype来简化函数指针代码,但需要注意decltype返回函数类型,此时不会将函数类型自动转换成指针类型,因为decltype的结果是函数类型,所以只有在结果前加上*才能得到指针
        • image.png
      • 返回指向函数的指针
        • 要声明返回函数指针的函数,可以使用以下办法
          • 仍然可以使用类型别名
          • 和函数类型的形参不一样,返回类型不会自动地转换为指针,必须显式地将返回值指定为指针
          • 也可以不使用类型别名:int (*f1(int)) (int*, int);
            • 从内而外阅读:f1有形参列表,是一个函数,f1前面有* ,因此f1返回一个指针,指针类型本身也包含形参列表,因此指针指向函数,指向的函数返回值是int
          • 也可以使用尾置返回类型,声明一个返回函数指针的函数auto f1 (int) ->int (*) (int*, int)
      • 将auto和decltype用于函数指针类型
        • 如果明确知道返回的函数是哪一个,可以使用decltype简化书写函数指针返回同类型的过程。
          • 假如有以下两个函数,返回类型相同,此时可以编写第三个函数,通过某些条件返回一个指针,该指针指向两个函数中的一个
            image.png
          • 注意:当decltype运用于某个函数时,它返回的类型是函数类型而非种子很,要显式地加上*以表示需要返回指针而不是函数本身

1.7类

  • 类的基本思想:数据抽象和封装
    • 数据抽象是一种依赖于接口和实现分离的编程技术,类的接口包括用户能执行的操作;类的实现规则则包括类的数据成员、负责接口实现的函数以及定义类所需的各种私有函数
    • 封装实现了类的接口和实现的分离,封装后的类隐藏了它的实现细节,也就是说,用户只能使用接口而无法访问实现部分
    • 类想要实现数据抽象和封装,首先需要定义一个抽象数据类型,在抽象数据类想着,由类的设计者负责考虑类的实现过程;使用类的程序员只需要抽象地思考类型做了什么,而不需要了解类型的工作细节
  • 一、定义抽象数据类型
    • 设计类接口时,应考虑如何才能使类易于使用,而使用类的时候不需要顾及类的实现机理
      • 类对象的基本空间是对象的非静态数据成员所占用的空间总和,成员函数为该类的所有成员共享

    • 定义和声明成员函数的方式与普通函数差不多,成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部,作为接口组成部分的非成员函数的定义和声明都在类的外部,类体外定义成员函数时,必须使用如下格式:名字空间名::类名::成员函数名
    • 定义在类内部的函数是隐式的inline函数
    • image.png
    • 定义成员函数
      • 成员函数必须在类内部声明,但是可以定义在类内也可以定义在类外
      • this介绍
        • 调用成员函数的时候,使用点运算符访问成员并进行调用。
        • 另一种调用方法:我们调用成员函数的时候,实际是在替某个对象调用它。
        • 成员函数通过一个名为this的额外的隐式参数来访问调用它的对象,当我们调用一个成员函数的时候,用请求函数的对象地址初始化为this
          • 例如,total.isbn();编译器负责把total的地址隐式地传递给this,使用伪代码表示大致为Sales_data::isbn(&total),调用Sales_data的isbn成员时传入了total的地址
        • 在成员函数内部,可以直接使用该函数的对象的成员而无需通过成员访问运算符来做到这一点因为this会指向这个对象。任何对类成员的直接访问都被看做this的隐式调用,也就是说,当isbn使用bookNo时,它隐式地使用this指向的成员,就像this->boosNo一样
        • this形参是隐式定义的,任何自定义名为this的参数或者变量行为都是非法的,可以在成员函数内部使用this
          1
          2
          3
          std::string isbn()const{return bookNo;}
          std::string isbn()const{return this->bookNo;}
          //两句相同,即在成员函数中,对于调用成员函数的对象的成员可以直接使用,不许要用点运算符等
        • this是一个常量指针,不允许改变this中保存的地址
      • const成员函数 常量成员函数
        • 在函数列表之后紧跟const关键字,const作用是修改隐式this指针类型,默认情况下,this的类型是指向类类型非常量版本的指针常量,尽管this是隐式的,但它仍然要遵循初始化规则,即我们不能把this绑定在一个常量对象上(原因见下红字),因此我们就不能在一个常量对象上调用普通的成员函数,这时就需要将this设置为指向常量的指针,有助于提高函数的灵活性。
        • this是隐式的,不会出现在函数列表中,C++允许把const关键字放在成员函数的参数列表之后,此时紧跟在参数列表后面的const表示this是一个指向常量的指针常量(常量指针常量)const *const this,此时函数不可以改变调用它的对象的内容,达成“只读”的特点。例:double avg_proce()const;
        • 常量对象只能调用const成员(常量对象的this指针是常量指针常量)
        • 不能给函数传入常量引用或常量指针来限制对于对象成员的写入(试图达到常量成员函数的作用),如果对常量引用或常量指针对象使用非const成员函数,会导致编译错误
          (常量指针常量不能作为右值赋给非常量指针常量this(会导致底层const的丢失))
          (类似地,对于对象的常量引用,编译器会认为是用常量对象初始化了非常量指针常量this,导致底层const丢失)
      • 类作用域和成员函数
        • 类本身就是一个作用域,类的成员函数的定义嵌套在类的作用域内。
        • 成员函数体可以随意使用类中的成员而无需在意成员出现的顺序,因为编译器处理类时会先编译成员的声明,之后再对成员函数体进行声明。
      • 在类外部定义成员函数
        • 在类外部定义函数时,成员函数的定义必须与它的声明匹配,也就是说,返回类型、参数列表和函数名都需要与类内部的声明保持一致,如果成员被审核吗成常量成员函数,那么它的定义也必须在参数列表后明确指定const属性,同时,类外部定义的成员名字必须包含它所属的类名,即函数名前必须要有类名::函数名来表示
      • 定义一个返回this对象的函数
        • 当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符;例如对于一个行为类似赋值运算符的函数来说,赋值运算符会将其左侧运算对象当成左值返回,因此若设计一个相加函数,则必须返回引用类型
        • 无需使用隐式的this指针来访问函数调用者的某个具体成员,需要把调用函数的对象当成一个整体访问,因此可以有return *this;该语句中,解引用this指针以获得执行该函数的对象,即返回total的引用
    • 定义类相关的非成员函数
      • 定义非成员函数的方式和其它函数一样,通常把函数的声明和定义分开,若函数在概念上属于类但不定义在类中,它一般应与类声明在同一个头文件内,这样保证用户使用类后只需要引入一个文件
    • 构造函数的基础知识,后续:7.5,15.7,18.1.3
      • 每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数;构造函数用于初始化类对象的数据成员,只要类的对象被创建,就会执行构造函数

      • 构造函数的名字和类名相同,但构造函数没有返回类型,除此之外类似于其他函数,构造函数也有(可为空)的参数列表和一个(可为空)的函数体,类可以包含多个构造函数,与重载函数类似,不同的构造函数之间必须在参数数量或参数类型上有所区别;
        构造函数不能被声明成const的,当创建一个const对象时,直到构造函数完成初始化过程,对象才能取得“常量”属性,因此构造函数才可以向consy对象的构造过程中向其写值。

      • 合成的默认构造函数

        • 类通过一个特殊的构造函数来控制默认初始化的过程,这个函数叫做默认构造函数,默认构造函数无需任何实参
        • 如果类没有显式地定义构造函数,那么编译器就会隐式地定义一个默认构造函数。编译器构造的函数又被称为合成的默认构造函数,对大多数类来说,合成的默认构造函数会按照如下规则初始化类的数据成员:
          • 若存在类内的初始值,则用它来初始化成员
          • 否则,默认初始化该成员(对于全局对象、静态全局对象和静态局部对象初始化为0;但对于局部自动对象以及动态对象,值不可预知)
        • 一旦定义了构造函数,系统将不再提供默认构造函数,若仍需要默认构造函数,
          1.可以重载默认构造函数(创建一个无需参数的构造函数),可以是提供了默认值的形参或无参函数
          1. C++11中规定,如果需要默认行为,可以通过在参数列表后加上= default来要求编译器生成构造函数,其中= default既可以声明一起出现在类内部,也可以作为定义出现在类的外部
      • 某些类不能依赖于合成的默认构造函数

        • 合成的默认构造函数只适合于简单的类,对普通的类来说,必须要定义他自己的默认构造函数,原因有下
          • 如果定义了一些其他的构造函数,除非再定义一个默认的构造函数,否则类将没有默认构造函数
          • 对于某些类来说,合成的默认构造函数可能执行错误的操作
          • 有时编译器不能为某些类合成默认的构造函数,如,类中包含一个其它类类型成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员
      • 定义构造函数的规则以及方法

        • 示例构造:
          • image.png
        • 定义了构造函数之后,也需要定义一个默认构造函数;
          • 默认构造函数不接受任何参数,定义构造函数的目的仅仅是因为既需要其他形式的构造函数,也需要默认的构造函数,同时它的作用应该完全等同于合成的默认构造函数
          • C++11中规定,如果需要默认行为,可以通过在参数列表后加上= default来要求编译器生成构造函数,其中= default既可以声明一起出现在类内部,也可以作为定义出现在类的外部
          • 和其它函数一样,如果= default在类的内部,则默认构造函数是内联的,如果它在类的外部,则该成员默认情况下不是内联的
          • 对于本章示例的类Sales_data来说,其默认构造函数可以写作Sales_data() = default;
        • 构造函数初始值列表
          • image.png
          • 如上示例构造函数中冒号以及冒号和花括号之间的代码被称作构造函数初始值列表,它负责为新创建的对象的一个或几个数据成员赋初始值,构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的成员初始值,不同成员的初始化通过逗号分隔开来
          • 当构造函数只显式初始化小于总成员数的成员时,其它的成员会被用与合成默认构造函数相同的方式进行初始化,此例中,只接受一个string参数的构造函数等价于
            • image.png
          • 通常情况下,建议使用类内初始值,因为类内 初始值确保成员被赋予一个正确的值,如果编译器不支持类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员
          • 构造函数的函数体都是空的,因为构造函数的唯一目的就是为数据成员赋值,一旦没有其它任务执行,则其函数体也为空
        • 在类外部定义构造函数
          • 在外部定义的构造函数也与其它成员函数一样,需要指明类成员,且它的函数名应该与类名一致,表示它是构造函数。外部定义的构造函数的函数初始值列表可以为空或不全,没有出现在构造列表中的成员将通过相应的类内初始值初始化,或者默认初始化,再执行构造函数体。
          • 示例: image.png
            此函数中read函数
            image.png
            this将对象当成一个整体来访问,*this将“this”对象作为实参传递给read函数
    • 拷贝、赋值和析构
      • 除了定义类的对象如何初始化,类还需要控制拷贝、赋值和销毁对象时发生的行为。如果不主动定义这些操作,编译器会合成相应的操作。一般来说,编译器生成的版本将对对象的每个成员进行拷贝、赋值和销毁操作。
      • 与构造函数一样,某些类不能依赖于合成的版本
        • 当类需要分配类对象之外的资源时,合成的版本常常会失效,例如,管理动态内存的类通常不能依赖于上述操作的合成版本
        • 很多需要动态内存的类能使用vector对象或string对象管理必要的储存空间,使用vector或string的类能避免分配和释放内存带来的复杂性,如果类包含vector或string成员,则其拷贝、赋值和销毁的合成版本可以正常工作,当对含有vector成员的对象执行拷贝或赋值时,vector会设法卡贝或赋值成员中的元素,当该对象被销毁时,将销毁vector对象(依次销毁vector中的每个元素)这点与string类似
        • 在直到 如何自定义操作之前,类中所有分配的资源都应该直接以类的数据成员的形式储存
  • 二、访问控制与封装
    • 访问说明符:用于加强类的封装性
      • 定义在public说明符后的成员在整个程序内可被访问,public成员定义类的接口
      • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(隐藏了)类的实现细节
      • image.png
      • 作为接口的一部分,构造函数和部分成员函数紧跟在public说明符后,而数据成员和作为实现部分的函数则跟在private说明符后
      • 一个类可以包含0个或多个访问说明符,而且对于某个访问说明符能出现多少次也没有严格规定,每个访问说明符制定了接下来成员的访问级别,其有效范围直到出现下一个访问说明符或到达类的结尾处为止
    • class或struct关键字
      • 可以使用这两个关键字中的任何一个定义类,唯一的区别在于两者访问权限不太一样
      • 类可以在它的第一个访问说明符之前定义成员,对这种成员的访问权限依赖于类定义的方式,如果使用struct关键字,则定义在第一个访问说明符之前的成员是public的,反之,如果使用class关键字,则这些成员是*private的
      • 如果希望定义类的所有成员时public的时,使用struct,反之用class
    • 友元
      • private的数据成员不可以被其它类或函数访问,需要令其它类或者函数成为它的友元才可以达成,成为友元只需要增加一条以firend关键字开始的函数声明语句即可;友元的声明只能出现在类定义的内部,但是在类内出现的具体位置不限,友元不是类的成员也不受它所在区域访问控制级别的约束;
        • 一般来说,在类定义开始或结束前的尾置集中声明友元
        • image.png
      • 封装优点:
        • 确保用户代码不会无意间劈坏封装对象的状态
        • 被封装的类的具体实现细节可以随时改变,无需调整用户级别的代码
        • 但类被改编后,还是需要重新编译代码的
      • 友元的声明
        • 友元声明仅仅指定了访问权限,不是一个通常意义上的函数声明,因此在友元声明之外再专门对函数进行一次声明
        • 一般友元声明与类本身放在同一个头文件中,同时还要对该函数提供独立的声明
        • 一些编译器允许在尚无友元函数的初始声明的情况下就调用它,但是建议最好对友元函数先提供一个独立的函数声明
  • 三、类的其他特性
    • 类成员再探
      • 定义类型成员
        • 除了定义数据和成员函数之外,类还可以自定义某种类型在类中的别名(typedef或者using都可),别名同样也可以设置访问限制,可以是private或public中的一种。在类中使用别名可以隐藏类实现的细节。
        • 用来定义类型的成员必须先定义后使用,因此类型成员一般都在类开始的地方
      • 令成员作为内联函数
        • 规模较小的函数适合被声明为内联函数,定义在类内部的函数时自动inline的。
        • 我们可以在类的内部把inline作为声明的一部分显式地声明成员函数,也可以在类的外部用inline关键字修饰函数的定义(即,类内部显式声明或类外部定义时二者有一用关键字inline修饰就可以被认定为内联函数)
        • 最好只在类外部定义的地方说明inline
        • inline成员函数也应该与相应的类定义在同一个头文件中
        • image.png
      • 重载成员函数
        • 和非成员一样,成员函数也可以被重载,只要函数之间在参数的数量和/或类型上有所区别就行。成员函数匹配的过程也与非常原函数非常类似
      • 可变数据成员
        • 如果需要修改类的某个数据成员,甚至是在某个const成员函数内进行修改,可以通过在变量声明中加入mutable关键字做到这一点
        • 一个可变数据成员永远不会是const,即使它是const对象成员
        • mutable修饰的可变成员可以被任何一个成员函数(包括const函数)改变值
      • 类数据成员的初始值
        • 如果要定义类类型的数据成员,最好在定义时拥有一个初始化的值。C++11中,最好的方式是把默认值声明成一个类内初始值。
        • 初始化类类型成员时,需要为构造函数传递一个符合成员类型的实参。
          类内初始值必须使用=的初始化形式(Screen的数据成员初始化使用),或者花括号括起来的直接初始化形式(下图)
        • image.png
    • 返回*this的成员函数(返回执行函数的对象的引用)
      • 返回引用的函数是左值的,意味着函数若返回*this的话,返回的是对象本身而非对象的副本,对于这种返回对象本身的函数则可以将许多成员函数在同一个对象上执行,例如下,值得注意的是,将一系列操作连接在一条表达式与分开两句的执行效果一致
        • image.png
          1
          2
          3
          4
          5
          myScreen.move(4,0).set('#');
          //等价于
          myScreen.move(4,0);
          myScreen.set(‘#’);

      • 但如果这两个函数的返回类型不是引用,那么move函数返回值将是*this的副本,因此后续调用set只能改变副本的值,不能改变myScreen的值
        1
        2
        3
        //如果返回值不是引用,等价于
        Screen temp = myScreen.move(4,0);
        temp.set('#');
      • 从const成员函数返回*this
        • 一个const成员函数如果以引用形式返回this,那么它的返回类型将是常量引用;如果想要让某些函数嵌入到一组动作序列中*,就要保证对于非常量的对象在调用后仍能保证非常量的返回;因此有了如下解决方法
      • 基于const的重载
        • 通过区分函数是否是const的,可以对其进行重载,重载原因:非常量版本的函数对于常量对象是不可用的, 所以我们只能在一个常量对象上调用const成员函数;虽然非常量对象可以同时调用常量或非常量版本的函数,但是调用非常量版本要好过常量版本
        • 可以定义一个private函数,负责执行函数的功能,再定义两个重载的成员函数,一个返回的是非常量引用,一个返回常量引用。
        • 当一个成员调用另外一个成员时,this指针在其中隐式地传递。当函数调用执行功能的private函数时,它的this指针隐式地传递给private函数。当非常量版本函数调用private函数时,它的this指针将隐式地从指向非常量的指针转换成指向常量的指针(因为例中的do_display函数是const函数),当private函数完成后,函数再各自返回解引用this所得的对象。再非常量版本中,this指向一个非常量对象,因此函数返回一个非常量的普通引用,而const版本则返回一个常量引用
          • image.png
    • 类类型
      • 对于两个类来说,即使常用完全一样,两个类也是不同的类型,对于一个类来说,它的成员和其他类的成员没有任何关系
      • 可以把类名作为类型的名字使用,从而直接指向类类型,也可以把类名跟在关键字class或struct后
        • image.png
        • 两种方式等价,第二种方法从C语言继承而来,在C++中也是合法的
      • 类的声明
        • 可以把类的声明和定义分开(像函数一样),这种声明被叫做前向声明,如,class Screen;在它声明之后定义之前是一个不完全类型,就是说,此时直到Screen是一个类类型,但是不清楚到底包含哪些成员
        • 不完全类型只能在有限的情景下使用:可以定义指向这种类型的指针或引用,也可以声明以不完全类型作为参数或返回类型的函数
        • 创建一个类对象之前,该类一定被定义过,而不能仅仅被声明,否则编译器无法了解这样的对象需要多少储存空间;同理,类对象的指针或引用也需要在定义后才能使用
        • 一个类的成员类型中不能有他自己,但是类中允许包含指向它自身类型的引用或指针(类名字出现后就表示被声明过,故可以…使用指针)
    • 友元再探
      • 类可以把其他类的成员函数定义成友元,友元函数还可以定义在类的内部,这样该函数就是隐式内联的
      • 类之间的友元关系
        • 直接在类中添加friend class或friend struct就可,则该友元类的成员函数可以访问此类包括非公有成员在内的所有成员
        • 友元关系不存在传递性,一个类的友元不能因为它是一个另一个类的友元而哪些友元就成为那个类的友元
      • 令成员函数作为友元
        • 除了令整个类作为友元之外,还可以只为相应函数提供访问权限,即可以把一个这个类需要访问另外类成员的函数声明成那个类的友元
        • 把一个成员函数声明成友元时,必须明确指出该成员函数属于哪个类,即要使用friend 类名::函数名;的形式进行友元声明
        • 同时还要注意程序的结构的组织(如果a类中某函数需要调用b类的private成员)
          • 首先要定义a类,声明要调用b类成员的成员函数c,但是不定义它,在该c函数使用b类成员之前必须先声明b类
          • 接下来定义b类,包括对于c函数的友元声明
          • 最后定义c函数,这是c可以使用b类成员
      • 函数重载和友元
        • 如果一个类想把一组重载函数声明成为友元,那么他需要对这组函数中的每一个分别声明
      • 友元声明和作用域
        • 类和非成员函数的声明不是必须在他们的友元声明之前,当一个名字第一次出现在友元声明中时,我们隐式地假定该名字在当前作用域中是可见的,但友元本身不一定真的声明在当前作用域中,就算在类的内部定义该函数,也必须在类外部提供相应的声明从而使得函数可见,仅仅用声明友元的类的成员调用该友元函数也必须确保函数被声明。
        • 友元声明与一般韩式声明不同,友元声明并非普通意义上的声明,因此友元声明不可以替代普通的函数声明
        • image.png
  • 四、类的作用域
    • 每个类都有自己的作用域,在类的作用域之外,普通数据和函数成员只能由对象、引用或指针使用成员访问运算符来访问。对于类类型成员来说,使用作用域运算符后的名字都必须是对应类的成员
    • 作用域和定义在类外部的成员
      • 一个类就是一个作用域:在类外部定义成员函数时必须同时提供类名和函数名(因为在类外部,成员的名字便被隐藏起来了);因此类名代表着剩余的部分(参数列表和函数体)在类的作用域之内,即我们可以使用类的其它成员而无需再次授权
      • 函数的返回类型通常出现在函数名之前,因此当成员函数定义在类外部时,返回类型中使用的名字都位于类的作用域之外,此时,返回类型必须说明它是哪个类的成员
        即如果a类中的成员函数b在外部定义,且要返回a类中的一个成员c,那么代码应该如下a::c a::b ( ) { }
    • 名字查找与类的作用域(?
      • 一般的名字查找过程
        • 在名字所在块中寻找其声明语句,只考虑在名字的使用之前出现的声明
        • 若没找到,继续查找外层作用域
        • 若最终没有匹配的声明,则程序报错
      • 类内部的成员函数中,解析名字的方式有所不同
        • 编译成员的声明
        • 直到类全部可见后才编译函数体(编译器处理完类中全部声明后才处理成员函数的定义)
      • 成员函数体直到整个类可见后才会被处理,所以它可以使用类中定义的任何名字,但如果。
      • 用于类成员声明的名字查找
        • 上述类内的两阶段处理方式只适用于成员函数中使用的名字,对于声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见,如果某个成员的声明使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找(即类外的区域)
          • image.png
      • 类型名要特殊处理
        • 一般来说,内层作用域可以重新定义外层作用域中的名字,即使改名字已经在内层作用域中使用过,但在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字
          • image.png
          • 但是,即使Account中定义的Money类型与外层作用域一致,上述代码仍然是错误的,尽管重新定义类型名字是一种错误行为,但一般编译器仍能顺利通过这样的代码,而忽略代码有错的事实。
        • 类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后
      • 成员定义中的普通块作用域的名字查找
        • 成员函数中使用的名字按照如下方式解析
          • 首先,成员函数内查找该名字的声明,只有在函数使用之前出现的声明才被考虑
          • 如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员都可以被考虑
          • 如果类内也没有找到该名字的声明,那么在成员函数定义之前的作用域内继续查找
          • 不建议使用其它成员的名字作为某个成员函数的参数
            • image.png{:height 535, :width 514}
      • 解决方法:给参数起一个另外的名字,不要把成员的名字作为参数或其它局部变量的使用
        • image.png
        • 尽管外层对象被隐藏,但是我们仍然可以用作用域运算符进行访问
      • 在文件中名字的出现处对其进行解析
        • 成员定义在类的外部时,名字查找的第三步不仅要考虑吧类定义之前的全局作用域中的声明,还需要考虑在成员函数定义之前的全局作用域中的声明
          • image.png
  • 五、构造函数再探
    • 构造函数初始值列表
      • 平时定义变量时一般用的是立即初始化的方式,并非先定义再赋值
      • 就对象的数据成员而言,初始化和赋值也有类似区别,如果没有在构造函数的初始值列表中显式地初始化成员,那么该成员将在构造函数体之前执行默认初始化
      • 例子:
        • image.png
        • 此例:对数据成员执行了赋值操作;也就是说,在函数体执行之前,已经对其进行了默认初始化,而后在函数体内对其赋值
    • 构造函数的初始值有时必不可少
      • 如果成员是const或引用的话,必须将其初始化,(const不能被赋值,引用必须被初始化)类似地当成员属于某种类类型且该类没有定义默认构造函数时,也必须这个成员初始化;
      • 随着构造函数体一开始执行,代表着初始化完成了,因此初始化const或引用类型的数据成员的唯一机会就是通过构造函数初始值
        • image.png
        • image.png
      • const、引用或属于某种未提供默认构造函数的类类型,必须通过构造函数初始值列表为这些成员提供初值
    • 成员初始化的顺序
      • 构造函数初始值列表只说明了用于初始化成员的值,但是没有限制初始化的具体执行顺序;成员初始化顺序与它们在类定义中出现的顺序一致,第一个成员先被初始化,之后类推;构造函数初始值的前后位置关系不会影响实际初始化顺序
      • 如果一个成员使用另外一个成员来初始化,那么需要注意初始化的顺序;有的编译器在数据成员顺序与成员声明顺序不符时会生成警告信息;最好令构造函数初始值的顺序与成员声明顺序保持一致,同时尽量避免使用某些成员初始化其它成员
    • 默认实参和构造函数
      • 如果一个构造函数为所有参数都提供了默认实参,那么它实际上也定义了默认构造函数
      • 例如:
        • image.png
        • 第一个构造函数其实就和默认构造函数一样(因为功能一样);同时,不用提供实参也可以调用这个构造函数,他其实就是一个默认构造函数
    • 委托构造函数
      • C++11扩展了构造函数初始值的功能,可以定义委托构造函数。委托构造函数使用它所属类的其它构造函数执行它自己的初始化过程,或是将它自己的一些职责委托给了其它构造函数
      • 委托构造函数也有成员初始值列表和一个函数体,在委托构造函数内,成员初始值列表只有一个唯一的入口,即类名本身。和其它成员初始值一样,类名后紧跟圆括号括起来的参数列表,参数列表中必须与类中另外一个构造函数匹配。
      • 例子如下:
        • 第一个构造函数接受三个实参,用它们来初始化数据成员,然后结束工作。第二个构造函数则等同于默认函数,默认构造函数令其使用三参数的构造函数完成初始化过程。接受strng参数的版本也同样委托了第一个构造函数
        • 接受istream&的构造函数也是委托构造函数,它委托给了默认构造函数,之后默认构造函数又委托给了第一个构造函数,这些受委托的构造函数执行完后,接着执行istream&构造函数的函数体,将调用read函数读取给定的istream
        • image.png
      • 如果受委托的构造函数函数体内有代码的话,将会先执行这些代码,之后才会将控制权交还给委托者的函数体
    • 默认构造函数的作用
      • 当对象被默认初始化或值初始化时自动执行默认构造函数,默认初始化的发生情况:
        • 在块作用域内不使用任何初始值定义一个非静态变量或数组时
        • 当一个类本身含有类类型的成员且使用合成的默认构造函数时
        • 当类类型的成员没有在构造函数初始值列表中显式地初始化时
      • 值初始化在以下情况发生:
        • 在数组初始化过程中如果我们提供的初始值数量少于数组的大小时
        • 当我们不使用初始值定义一个局部静态变量时
        • 当我们通过书写形如T()表达式显式地请求初始化时,其中T是类型名
          (vector的一个构造函数只接受一个实参用于说明vector大小,他就是使用一个这种形式的实参来对它的元素初始化器进行初始化)
      • 类必须包含一个默认构造函数以便在上述情况下使用 未定 书p262
        • image.png
      • 使用默认构造函数
        • 定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉对象名之后的空括号对
          1
          2
          Sales_data obj ();//错误:定义了一个函数而非对象
          Sales_date obj;//正确,obj是一个默认初始化对象
      • 隐式的类类型转换
        • 可以为类定义隐式转换规则,如果构造函数只接收一个实参,那它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数
        • 能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则
        • 例子:
          • string实参调用Sales_data的combine成员,调用时,编译器会用给定的string自动创建一个Sales_data对象,之后新生成的对象被传递给combine,combine参数是一个常量引用,因此可以给给参数传递一个临时量
          • image.png
      • 只允许一部类类型转换
        • 编译器只会自动地执行一步类型转换,因此如果代码包含两种转换规则,则会出现错误
          • 例: image.png
        • 如果要完成上述调用,可以先显式地将字符串转为string或转换成Sales_data对象
          • 例: image.png
      • 抑制构造函数定义的隐式转换
        • 要求隐式转换的程序上下文中,可以通过将构造函数声明为explicit加以阻止(加在函数名前);|
          explicit只对一个实参的构造函数有效,需要多个实参的构造函数不能用于执行隐式转换,所以无需将它们指定为explicit的;
          explicit只能在类内声明构造函数时使用,在类外部定义时不应重复
      • explicit构造函数只能用于直接初始化
        • 执行拷贝初始化时可能会发生隐式转换,此时我们只能使用直接初始化而不能使用explicit构造函数
          • 例:
            image.png
        • explicit关键字声明构造函数时,它将只能以直接初始化形式使用,且编译器不会再自动转换过程中使用该构造函数
      • 为转换显式地使用构造函数
        • 尽管编译器不会使用explicit的构造函数用于隐式转换,但我们可以使用这些构造函数显式地强制进行转换
          • 例: image.png
            static_cast使用执行了显式的而非隐式的转换,static_cast使用istream构造函数构造了一个临时的Sales_data对象
      • 标准库中含有显示构造函数的类
        • 标准库中的类含有单参数的构造函数
          • 接受一个单参数的const char*的string构造函数不是explicit的
          • 接受一个容量参数的vector构造函数时explicit的
    • 聚合类
      • 聚合类是的用户可以直接访问其成员,并且具有特殊的初始化语法形式,当一个类满足如下形式时,可以说它是聚合的
        • 所有成员都是public的
        • 没有定义任何构造函数
        • 没有类内初始值
        • 没有基类,也没有virtual函数
      • 可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员;初始值的顺序必须与声明顺序一致,第一个成员初始值要放在第一个,以此类推…
        • 例:
        • image.png
          image.png
      • 如果初始值列表中的元素个数少于类成员的竖立,则靠后的成员被值初始化,初始值列表的元素个数绝对不能超过类成员的竖立
      • 显式地初始化类的对象存在三个明显缺点
        • 要求类的所以成员都是public的
        • 将正确初始化每个对象的每个成员的重任交给了类的用户,但用户容易忘掉某个初始值,或提供一个不恰当的初始值,容易出错
        • 添加或删除一个成员之后,所有的初始化语句都需要更新
    • 字面值常量类
      • 除了算术类型、引用和指针外,某些类也是字面值类型,和其他类不同,字面值类型的类可能含有constexpr函数成员,这样的成员必须符合constexpr函数的所有要求,它们是隐式const的
      • 数据成员都是字面值类型的聚合类是字面值常量类;如果一个类不是聚合类,但它符合下属要求,则它耶斯一个字面值常量类:
        • 数据成员都是字面值类型
        • 类必须至少含有一个constexpr构造函数
        • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式,或如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
        • 类必须使用析构函数的默认定义,该成员负责销毁类的对象
      • constexpr构造函数
        • 尽管构造函数不能是const的,但字面值常量类的构造函数可以是constexpr函数,一个字面值常量类必须至少提供一个constexpr构造函数
        • constexpr构造函数可以声明成=default的形式,(或是删除函数的形式),否则,constexpr构造函数就必须既符合构造函数的要求(不能有返回语句)又要符合constexpr函数的要求(拥有的唯一可执行语句时返回语句);因此constexpr构造函数体一般来说时空的,可以通过前置关键字就可以声明一个constexpr构造函数
          • 例: image.png
        • constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数,或是一条常量表达式
        • constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型
  • 六、类的静态成员
    • 参考C++ static、const 和 static const 类型成员变量声明以及初始化 | 菜鸟教程 (runoob.com)
    • 类有时需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联(即,并非类中每个对象需要与该成员相关;同时还希望该成员都能使用随时可能更新的该成员的新值)此时就需要静态成员
    • 声明静态成员
      • 通过在成员的声明之前加上关键字static使得其与类关联在一起,静态成员可以是public或private的,静态数据成员的类型可以是常量、引用、指针、类类型等;一般声明在类内,定义在类外部
      • 类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据;对于如下例子,每个Account对象将包含两个数据成员,owner和amount,只存在一个interestRate对象且它被所有Account对象共享。
        • 例子: image.png
      • 静态成员函数也不与任何对象绑定在一起,它们不包含this指针,作为结果,静态成员只能调用static函数,静态成员函数不能声明成const的,而且也不能在static函数体内使用this指针(因此static函数不能访问类中其他成员,只能访问静态成员和静态函数)。这一限制既适用于this的显式使用,也对调用非静态成员的隐式使用有效
    • 使用类的静态成员
      • 可以使用作用域运算符直接访问静态成员,即静态函数可以直接使用类来调用而不需要创建对象
        • 例:
          image.png
      • 虽然静态成员不属于类的某个对象,但是可以使用类的对象、引用或指针来访问静态成员
        • 例: image.png
      • 成员函数不需要通过作用域运算符就可以直接使用静态成员
    • 定义静态成员
      • 定义静态成员函数:既可以在类的内部,也可以在外部定义静态成员函数,当在外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句
        • 例:对于上文出现的类Account中的rate函数
          1
          2
          3
          4
          void Account::rate(double newRate)
          {
          interestRate = newRate;
          }
      • 因为静态数据成员不属于类的任何一个对象,因此它们不是在创建类的对象时被定义的,这意味着它们不是由类的构造函数初始化的。一般来说,我们不在类的内部初始化静态成员,相反地,必须在类的外部定义和初始化每个静态成员,一个静态数据成员只能定义一次
      • 类似于全局变量,静态数据成员定义在任何函数之外,一旦它被定义,就将一直存在于程序的整个生命周期中
      • 定义静态数据成员的方式和在类的外部定义成员函数差不多,需要指定对象的类型名,然后是类型名、作用域运算符和成员的名字
        • 例如:double Account ::interestRate = initRate();此例中,静态成员interestRate的值由函数initRate决定
      • 由于从类名开始,定义语句的剩余部分就位于类的作用域之内了,因此可以直接用initRate函数(虽然它是私有的成员函数,但是从类名开始,代表这条语句的剩余部分都在类的作用域内,自然也可以直接使用private的static函数)
      • static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问;static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
        静态成员变量必须初始化,而且只能在类体外进行
      • 要想确保对象只被定义一次,应该把静态数据成员的定义与其它非内联函数的定义放在同一个文件中
    • 静态成员的类内初始化
      • 前置:关于const成员
        • const 成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数。
        • const 数据成员 只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象没被创建时,编译器不知道 const 数据成员的值是什么。
        • const 数据成员的初始化只能在类的构造函数的初始化列表中进行。要想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现,或者static cosnt。
      • 类的静态成员不应该在类内初始化。但可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,因为成员本身就是常量表达式,所以它们能用在适用于常量表达式的地方,例如,可以用一个初始化了的静态数据成员指定数组成员的维度
      • 如果某个静态成员的应用场景仅限于编译器可以替换它的值的情况,则一个初始化const或constexpr static不需要分别定义;如果我们将它用于值不能替换的场景中,则该成员必须有一条定义语句【即对该成员的值有明确规定】
        • 例如:下图就为在类内部的定义 image.png
      • 如果类内部提供了一个初始值,那么成员的定义不能再指定一个初始值了,但是仍然需要定义,如constexpr int Account::period;const int Account::i
      • 既是一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类外部定义一下该成员
    • 静态成员能用于某些场景,而普通成员不能
      • 静态成员独立于任何对象,因此在某些非静态数据成员可能非法的场合,静态成员却可以正常使用。静态数据成员可以是不完全类型。静态数据成员的类型可以就是它所属的类类型,而非静态数据成员则受到限制,只能声明它所属类的指针或引用
        • image.png
      • 静态成员和普通成员的另一个区别是,可以使用静态成员作为默认实参(静态成员独立于成员,属于类);非静态成员不能作为默认实参,因为它的值本身属于对象的一部分
  • 附:类模板
    • 类模板以关键字template开始,后跟模板类型参数列表,用法类似函数模板
    • 类模板的成员函数也可以在类模板体外进行描述, 其中,template表示模板,T表示数据类型,模板名<T>是类模板完整的新式类名
    • 模板类:将类模板以及其中的形式参数换成实际数据类型而形成的类。模板类是对类模板的第一次实例化,用模板类创建对象是对类模板的第二次实例化
      用类模板创建对对象模板名 <实际数据类型名> 对象名 (初始化列表)

C++ Chap1
https://mapllle.site/2022/09/01/Language/CPP/CPPchap1/
作者
MAple
发布于
2022年9月1日
许可协议