当前位置: 首页 > 工具软件 > TH-Nebula > 使用案例 >

C++ Primer Plus 6th代码阅读笔记

朱运诚
2023-12-01

C++ Primer Plus 6th代码阅读笔记

  1. 第一章没什么代码

  2. 第二章代码

    • carrots.cpp :
      • cout 可以拼接输出,cin.get()接受输入
    • convert.cpp
      • 函数原型放在主函数前,int stonetolb(int); 1 stone = 14 pounds 一英石等于十四英镑
      • cin.get( )会读取输入字符,包括回车
    • ourfunc.cpp
      • using namespace std会在作用域里其作用,所以写在函数内的话,函数外不起作用
      • 两次cin >>count,正常运行,说明cin >>(int或char)接受非空格,函数重载
      • 对比cin.get()接受空格
      • 若先cin >>(int或char)再cin.get(),且输入为a和回车,回车会被cin.get()截得
  3. 第三章代码

    • arith.cpp
      • cout.setf(ios_base::fixed, ios_base::floatfield);
      • 第一个参数:fmtflags:bool,decimal-integer,hexadecimal-integer,floating
      • 第二个参数:scientific|fixed:floatfield
      • 通常cout会删除结尾的零。例如,将3333333.250000显示为3333333.25。调用cout.setf( )将覆盖这种行为,至少在新的实现中是这样的。
      • fixed:定点表示法
      • floatfield:科学定点
      • 如果cin >> hats; cin >> heads;cin.get(); == cin >> hats; cin.get(); cin >> heads;cin.get();
    • assign.cpp
      • 浮点数强制转为整数:直接去掉小数点后的
      • \a表示振铃
      • \b表示退格
    • devide.cpp
      • 浮点常量默认为double
      • 1.e7是double,1.e7f是float
      • 对于long double类型,可使用l或L后缀(由于l看起来像数字1,因此L是更好的选择)
      • short int 2bytes -32768 to 32767
      • int 4bytes -2147483648 to 2147483647
      • float 4bytes:大概也是10位,小数点后6位,不太清楚
      • double 8bytes,小数点后15位,不太清楚
      • 可以直接用INT_MAX来查,sizeof (int),sizeof 变量名
      • 所以int默认是有符号的int
    • fltadd.cpp
      • 程序将数字加1,然后减去原来的数字。结果应该为1,但是结果为0。问题在于,2.34E+22是一个小数点左边有23位的数字。加上1,就是在第23位加1。但float类型只能表示数字中的前6位或前7位,因此修改第23位对这个值不会有任何影响。
    • hexoct1.cpp
      • 0x42十六进制 042八进制
      • cout默认以十进制输出
    • hexoct1.cpp
      • cout << hex;可以更改为十六进制输出
    • modulus.cpp
      • int/int,int%int
    • morechar.cpp
      • cout.put(’!’);
    • typecast.cpp
      • static_cast<>可用于将值从一种数值类型转换为另一种数值类型。
      • 运算符static_cast<>比传统强制类型转换更严格
      • 可能会更安全
  4. 第四章代码

    • addpntrs.cpp

      • 数组头指针,直接数组名或者&数组名[0]
      • stacks[1] 等价于 *(stacks + 1)
      • sizeof(数组名) = 整个数组的大小,单位是字节,如果长度为10,int数组,10*4 = 40
      • sizeof(pw) 指针的大小都是8个字节(对于64位系统),不管是double指针和int指针
    • arraynew.cpp

      • double * p3 = new double [3]; // space for 3 doubles
      • 定义指针指向开辟的内存
      • delete [] p3;
    • arrsruct.cpp

      • 结构体定义

      • 定义完结构体,再定义结构体数组

      • 结构体有点像int类型,是一种类型

      • 结构体数组的定义及初始化

      • guests[0].volume来得到该…

      • 代码

      • inflatable guests[2] =          // initializing an array of structs
        {
            {"Bambi", 0.5, 21.99},      // first structure in array
            {"Godzilla", 2000, 565.99}  // next structure in array
        };			
        
    • choices.cpp

      • double a1[4] = {1.2, 2.4, 3.6, 4.8};
      • vector a2(4);
        • a2[0] = 1.0/3.0;
        • a2[1] = 1.0/5.0;
        • a2[2] = 1.0/7.0;
        • a2[3] = 1.0/9.0;
      • array<double, 4> a3 = {3.14, 2.72, 1.62, 1.41};
      • 索引-2是什么意思呢:找到a1指向的地方,向前移两个double元素,并将20.2存储到目的地。也就是说,将信息存储到数组的外面。与C语言一样,C++也不检查这种超界错误。这表明数组的行为是不安全的。
    • delete.cpp

      • 三种存储方式:自动存储,静态存储,动态存储。
      • 自动存储
        • 它们在所属的函数被调用时自动产生,在该函数结束时消亡
        • 实际上,自动变量是一个局部变量,其作用域为包含它的代码块。代码块是被包含在花括号中的一段代码。
        • 自动变量通常存储在栈中
      • 静态存储
        • static double fee = 56.60
        • 存在于程序的整个生命周期
      • 动态存储
        • new和delete运算符
        • 自由存储空间(free store)或堆(heap)
        • 程序员对程序如何使用内存有更大的控制权
        • 自动添加和删除机制使得占用的内存总是连续的,但new和delete的相互影响可能导致占用的自由存储区不连续,这使得跟踪新分配内存的位置更困难。
        • 如果使用new后不使用delete,会导致内存泄漏,该内存无法收回
        • C++智能指针有助于自动完成这种任务
    • instr1/2/3.cpp

      • 都是输入给char name[ArSize];而不是char * name;
      • char name[Arsize];
      • cin>>name;//不接受回车,空格,但是当输入“dec liu”时,会被看成两次输入
      • cin使用空白(空格、制表符和换行符)来确定字符串的结束位置,这意味着cin在
        获取字符数组输入时只读取一个单词。读取该单词后,cin将该字符串放到数组中,并自动在结尾添加空字符。
      • cin.getline(name,Arsize);//接受回车,空格
      • getline( )函数每次读取一行。它通过换行符来确定行尾,但不保存换行符。相反,
        在存储字符串时,它用空字符来替换换行符,空字符即字符串末尾。
      • 此时输入“dec liu”就正常
      • cin.get(name, ArSize).get();
      • get并不再读取并丢弃换行符,而是将其留在输入队列中
      • get()可以知道读取停止是因为已经读取了整行还是由于数组已填满呢
      • cin.getline()和cin.get()都有空行问题,读取到空行,会设置失效位,接着输入被阻断,但可以用cin.clear(),来继续输入。
      • 总结:
        • 第一种:怕长度长的,怕有空格的输入,会导致一次输入变成来两次输入
        • 第二种:怕长度长的和直接回车的,都会设置失效位,读取会停止
        • 第三种:可以可以知道读取停止是因为已经读取了整行还是由于数组已填满呢
    • mixtypes.cpp

      • 结构体指针
      • 结构体数组trio.trio是数组名,trio->year;数组名本质是地址,可以这样->,而不是.
      • auto的作用:以后再看
    • newstrcut.cpp

      • ps->name
      • (*ps).volume
    • numstr.cpp

      • 用户根本没有输入地址的机会。问题在于,当cin读取年份,将回车键生成的换行符留在了输入队列中。
    • ptrstr.cpp

      • cout<<字符串地址,输出字符串;可以接受char animal[20]的animal,const char * bird的bird

      • delete [] ps;

      • ps = animal;指向同地址

      • ps = new char[strlen(animal) + 1]; strcpy(ps,aniaml);

      • 可以cout<< (int *) animal来看animal字符串的地址

      • 如果直接char * ps;接着cin >> ps; 不行,因为ps的指针是未知的

      • 内存必须先申请再读写

      • strtype1/2/3/4.cpp

        • #include // C-style string library
        • C风格:strcpy,strcat,strlen
        • 未被初始化的数据,第一个空字符的出现位置是随机的,对于char charr[20];
        • cin.getline(charr, 20); charr的输入
        • getline(cin, str); str的输入
    • use_new.cpp

      • double * pd = new double; *pd = 10000001.0;
      • 地址在堆区,指针在栈区
  5. 第五章代码

    • bigstep.cpp

      • 可以单独using std::cout;
    • comstr1.cpp

      • for循环括号内,++的可以是char,判断条件可以是返回bool的函数,打破思维
    • dowhile.cpp

      • 对于先输入再判断的,可以用do while
      • 强行转换非0int转为bool是true,包括-1
    • express.cpp

      • 通常,cout在显示bool值之前将它们转换为int,但cout.setf(ios::boolalpha)函数调用设置了一个标记,该标记命令cout显示true和false,而不是1和0。
      • cout.setf(ios_base::boolalpha);
    • forstr2.cpp

      • string word; cin >> word;
      • 交换
        • temp = a
        • a = b
        • b = temp
      • for 循环里同时放两个变量,一个指前,一个指后
    • nested.cpp

      • \t制表符
    • textin1/2/3/4.cpp

      • cin<<ch 不接受空字符,是个问题

      • 可以在#后面输入字符,是个问题

      • 使用cin.get(char)进行补救,可以解决不接受空字符的问题

      • cin.get(char)是通过引用来修改值的,不同于使用变量的地址

      • linux系统的ctrl + d才是EOF,ctrl + z是暂时挂起,按fg可恢复

      • Windows系统的ctrl + z是EOF

      • 由于EOF表示的不是有效字符编码,因此可能不与char类型兼容。例如,在有些系统中,char类型是没有符号的,因此char变量不可能为EOF值(−1)。由于这种原因,如果使用cin.get( )(没有参数)并测试EOF,则必须将返回值赋给int变量,而不是char变量。另外,如果将ch的类型声明为int,而不是char,则必须在显示ch时将其强制转换为char类型。

      • 	int ch;                         // should be int, not char
            while ((ch = cin.get()) != EOF) // test for end-of-file
            {
                cout.put(char(ch));
            }
        
    • while.cpp

      • 循环字符数组
      • name[i] != ‘\0’
    • 头文件ctime定义了一个符号常量—CLOCKS_PER_SEC,该常量等于每秒钟包含的系统时间单位数,将系统时间除以这个值,可以得到秒数

  6. 第六章代码

    • cctypes.cpp

      • #include

      • 如果cin位于测试条件中,则将被转换为bool类型。如果输入成功,则转换后的值为true,否则为false

    • condit.cpp

      • cin >> a >> b;
      • 输入两个整数
    • enum.cpp

      • enum {red, orange, yellow, green, blue, violet, indigo};
      • 对应0-6
    • outfile.cpp

      • ofstream outFile;

      • outFile.open(“carinfo.txt”);

      • outFile << fixed;
        outFile.precision(2);
        outFile.setf(ios_base::showpoint);
        outFile << "Make and model: " << automobile << endl;
        outFile << "Year: " << year << endl;
        outFile << "Was asking $" << a_price << endl;
        outFile << "Now asking $" << d_price << endl;
        
        outFile.close();                // done with file
        
      • 这里的outFile有点类似于cout

    • sumafile.cpp

      • ifstream inFile;
        if (!inFile.is_open())  // failed to open file
        {
            cout << "Could not open the file " << filename << endl;
            cout << "Program terminating.\n";
            // cin.get();    // keep window open
            exit(EXIT_FAILURE);
        }
        while (inFile.good())   // while input good and not at EOF
        {
            ++count;            // one more item read
            sum += value;       // calculate running total
            inFile >> value;    // get next value
        }
        if (inFile.eof())
            cout << "End of file reached.\n";
        else if (inFile.fail())
            cout << "Input terminated by data mismatch.\n";
        else
            cout << "Input terminated for unknown reason.\n";
        if (count == 0)
            cout << "No data processed.\n";
        else
        {
            cout << "Items read: " << count << endl;
            cout << "Sum: " << sum << endl;
            cout << "Average: " << sum / count << endl;
        }
        inFile.close();  
        
      • 输入格式不对,如将字符输入给double型,看inFile.fail()

      • 到文件尾:inFile.eof()

  7. 第七章

    • arfupt.cpp

      • 函数声明
        • 参数列表const double ar [ ]与const double * ar的含义完全相同
        • const函数,const double * f1(const double ar[], int n);返回一个常量指针
      • 函数指针
        • 函数指针,将函数名改为(*p1)即可,也可以用auto p2 = f2
        • 函数指针调用时,可以加上(*p1)(av,3),也可以不用直接,p1(av,3)
      • 函数数组
        • pa是函数数组,不能直接auto pe = {f1,f2,f3};,要先定义pa这个函数数组,才能用auto pb = pa; pa的定义:函数指针数组,同理,改为(*pa[3])即可
        • *函数数组指针的使用pa [i] (av,3) 或者 (pb[i]) (av,3)
      • 数组名&数组名的区别
        • 数组名&数组名虽然在数值上相同,但是在类型上不同——数组名是指向首个元素的指针,&数组名是指向整个数组的指针。在指针加减整数的时候,前者以一个元素为颗粒度,后者以整个数组为颗粒度。
        • sizeof(array)是整个数组的
        • array的值就等于&array[0]的值
        • auto pc = &pa;用的时候加上*pc
      • store return value
        • const double * pdb = (*pd)[1] (av,3);
    • arrfun2.cpp

      • sum = sum_arr(cookies + 4, 4);
      • 函数参数是数组的时候,可以数组名的偏移,思维打开
    • arrfun3.cpp

      •  for (i = 0; i < limit; i++)
            {
                cout << "Enter value #" << (i + 1) << ": ";
                cin >> temp;
                if (!cin)    // bad input
                {
                    cin.clear();
                    while (cin.get() != '\n')
                        continue;
                   cout << "Bad input; input process terminated.\n";
                   break;
                }
                else if (temp < 0)     // signal to terminate
                    break;
                ar[i] = temp;
            }
        
      • 输入后,通过!cin判断是否是好的输入,若是不好,则cin.clear(),接着循环进行,直到get到回车,break;

      • void show_array(const double ar[], int n);

      • show的时候,要对这个数组加个const

    • arrfun4.cpp

      • int sum_arr(const int * begin, const int * end)
        {
            const int * pt;
            int total = 0;
        
            for (pt = begin; pt != end; pt++)
                total = total + *pt;
            return total; 
        }
        sum = sum_arr(cookies + 4, cookies + 8);  
        
    • arrobj.cpp

      • void fill(std::array<double, Seasons> * pa);修改要加指针,(*pa)[i];
      • fill(&expenses);
      • void show(std::array<double, Seasons> da);
      • show(expenses);
      • 函数fill()和show()都有缺点。函数show()存在的问题是,expenses存
        储了四个double值,而创建一个新对象并将expenses的值复制到其中的
        效率太低。如果修改该程序,使其处理每月甚至每日的开支,这种问题
        将更严重。show传参传入会复制一新对象,所以效率低下
    • funptr.cpp

      • 函数指针作为参数
      • void estimate(int lines, double (*pf)(int))
      • (*pf)(lines)
      • estimate(code, betsy);
      • double betsy(int lns)
    • lotto.cpp

      • unsigned本身是unsigned int的缩写
      • cin >> total >> choicesk可以作逻辑词
    • ruler.cpp

      • 包含多个递归调用的递归

      • subdivide( )函数使用变量level来控制递归层。函数调用自身时,将把level减1,当level为0时,该函数将不再调用自己。注意,subdivide( )调用自己两次,一次针对左半部分,另一次针对右半部分。最初的中点被用作一次调用的右端点和另一次调用的左端点。请注意,调用次数将呈几何级数增长。也就是说,调用一次导致两个调用,然后导致4个调用,再导致8个调用,依此类推。这就是6层调用能够填充64个元素的原因(2 6 =64)。这将不断导致函数调用数(以及存储的变量数)翻倍,因此如果要求的递归层次很多,这种递归方式将是一种糟糕的选择;然而,如果递归层次较少,这将是一种精致而简单的选择。

      • 画尺子

        • subdivide(ruler,min,max, i);

        • i=1分一次,i=2分两次

        • for (int j = 1; j < Len - 2; j++)
                    ruler[j] = ' ';  // reset to blank ruler
          void subdivide(char ar[], int low, int high, int level)
          {
              if (level == 0)
                  return;
              int mid = (high + low) / 2;
              ar[mid] = '|';
              subdivide(ar, low, mid, level - 1);
              subdivide(ar, mid, high, level - 1); 
          }
                      
          
    • strctptr.cpp

      • void rect_to_polar(const rect * pxy, polar * pda);

      • void show_polar (const polar * pda);

      • while (cin >> rplace.x >> rplace.y)
        {
            rect_to_polar(&rplace, &pplace);    // pass addresses
            show_polar(&pplace);        // pass address
            cout << "Next two numbers (q to quit): ";
        }
        //挺优雅的
        
    • strgback.cpp

      • // builds string made of n c characters
        char * buildstr(char c, int n)
        {
            char * pstr = new char[n + 1];
            pstr[n] = '\0';         // terminate string
            while (n-- > 0)
                pstr[n] = c;        // fill rest of string
            return pstr; 
        }
        
    • strgfun.cpp

      • const char * str, str是指针,可以++来遍历,*str是

      • unsigned int c_in_str(const char * str, char ch)
        {
            unsigned int count = 0;
        
            while (*str)        // quit when *str is '\0'
            {
                if (*str == ch)
                    count++;
                str++;        // move pointer to next char
            }
            return count; 
        }
        
    • structfun.cpp

      • #include

      • answer.distance =
                sqrt( xypos.x * xypos.x + xypos.y * xypos.y);
        answer.angle = atan2(xypos.y, xypos.x);
        
  8. 第八章代码

    • arrtemp.cpp

      • template <class T, size_t n>
        void display(const std::array<T, n> & ar);
        
      • 模板类声明

      • 这里用模板类

      • std::size_t是一个typedef,对应于合适的整型,在我的电脑上是long unsigned int

    • choices.cpp

      • template<class T>
        T lesser(T a, T b)  
        
      • 函数调用与模板函数和非模板函数都匹配,因此选择非模板函数,返回20。

      • lesser<>(m, n)有“<>”这个符号,一定是用模板

      • lesser(x, y),虽然输入x,y是double,但强行用int模板

    • filefunct.cpp

      • 方法setf( )返回调用它之前有效的所有格式化设置。

      • 同理,os.precision(0)也是

      • void file_it(ostream & os, double fo, const double fe[],int n)
        
        ios_base::fmtflags initial;
        initial = os.setf(ios_base::fixed, ios_base::floatfield);
        std::streamsize sz = os.precision(0);
        
      • ofstream是ostream的子类

    • funtemp.cpp

      • template <typename T>  // or class T
        void Swap(T &a, T &b)
        
    • inline.cpp

      • inline double square(double x) { return x * x; }
      • 整个函数定义都放在一行中,但并不一定非得这样做。然而,如果函数定义占用多行(假定没有使用冗长的标识符),则将其作为内联函数就不太合适。
      • 内联函数比宏好
      • 宏不能按值传递,如当输入参数为4.5+7或者输入参数为c++,就不行
    • left.cpp

      • char * left(const char * str, int n)
      • 返回数组时,可以在函数里面new: char * p = new char[n+1];
      • 但需要在外面delete
      • char * left(const char * str, int n = 1);在函数原型的时候,=1,可以默认参数,这样不输入n参数时,默认n=1
    • leftover.cpp

      • 计算位数如下:

      • unsigned digits = 1;
        while (n /= 10)
                digits++;
        
    • strquote.cpp

      • 用const 修饰函数的返回值,如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。

      • 例如函数:
        const char * GetString(void);
        如下语句将出现编译错误:
        char *str = GetString();
        正确的用法是
        const char *str = GetString();
        如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值。
        例如不要把函数int GetInt(void) 写成const int GetInt(void)。
        函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。
        class A  
        {  
           A & operate = (const A &other); // 赋值函数  
        };  
        A a, b, c; // a, b, c 为A 的对象  
          
        a = b = c; // 正常的链式赋值  
        (a = b) = c; // 不正常的链式赋值,但合法  
        如果将赋值函数的返回值加const 修饰,那么该返回值的内容不允许被改动。上例中,语句 a = b = c 仍然正确,但是语句 (a = b) = c 则是非法的。
        
      • const 成员函数(const的作用:说明其不会修改数据成员)

      • 如果在编写const 成员函数时,不慎修改了数据成员,或者调用了其它非const 成员函数,编译器将指出错误
        a. const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
        b. const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
        c. const成员函数不可以修改对象的数据,不管对象是否具有const性质.它在编译时,以是否修改成员数据为依据,进行检查.
        e. 然而加上mutable修饰符的数据成员,对于任何情况下通过任何手段都可修改,自然此时的const成员函数是可以修改它的。	
        
      • const引用可以读取但是不可以被修改引用对象,任何对const引用进行赋值都是不合法的,它适用指向const对象的引用,而非const的引用不适用于指向const对象的引用。

    • swaps.cpp

      • 通过引用来交换值

      • void swapr(int & a, int & b)    // use references
        {
            int temp;
        
            temp = a;       // use a, b for values of variables
            a = b;
            b = temp;
        }
        
    • twoswap.cpp

      • 可以提供一个具体化函数定义——称为显式具体化(explicit specialization),其中包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

      • template <typename T>
        void Swap(T &a, T &b);
        template <> void Swap<job>(job &j1, job &j2);
        //当参数为Job对象时,将使用显示具体化
        
    • tempover.cpp

      • 当重载函数
        • void ShowArray(T arr[], int n);
        • void ShowArray(T * arr[], int n);
        • 重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果将模板B从程序中删除,则编译器将使用模板A来显示pd的内容,因此显示的将是地址,而不是值。
  9. 第九章代码

    • external.cpp support.cpp

      • ::warming作用域解析运算符(::),放在变量名前面时,该运算符表示使用变量的全局版本。

      • extern double warming; // use warming from another file使用别的文件的全局变量,要用extern来声明它

      • CMakeLists.txt配置

      • add_library(support_library STATIC support.cpp )
        add_executable(my_cmake_exe external.cpp)
        target_link_libraries( my_cmake_exe support_library )
        
    • file1.cpp coordin.h file2.cpp

      • .h文件定义了结构体和函数原型
      • file2.cpp实现了头文件的函数
      • file1.cpp调用了该头文件中的函数
      • 这里如果头文件在已知目录下,add_library(support_library STATIC file2.cpp )会自动去已知目录下找coordin.h因为file2.cpp里面有#include “coordin.h”
    • namesp.h namesp.cpp usenmsp.cpp

      • “namesp.h“中:
        namespace pers
        {
            定义结构体;
            函数原型;
        }
        namespace debts
        {
            using namespace pers;//使用刚定义的名称空间
        }
        
      • “namesp.cpp“中:
        namespace pers
        {
           	写函数
        }
        
      • “usenmsp.cpp“中:
        using debts::Debt;
        using debts::showDebt;
        using namespace debts;
        
    • twofile1.cpp twofile2.cpp

      • twofile1.cpp使用twofile2.cpp的内容

      • twofile1.cpp中的int tom=3 twofile2.cpp使用extern int tom;

      • twofile1.cpp中的int dick = 30; twofile2.cpp使用static int dick = 10;

      • twofile1.cpp中的static int harry = 300; twofile2.cpp使用int harry = 200;

      • 默认是extern

      • 不能两个文件都int errors=3 这种做法将失败,因为它违反了单定义规则。file2中的定义试图创建一个外部变量,因此程序将包含errors的两个定义,这是错误。但如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量.

    • newplace.cpp

      • 定位new运算符

      • new运算符还有另一种变体,被称为定位(placement)new运算符,它让您能够指定要使用的位置。

      • char buffer[BUF]; // chunk of memory静态区

      • (void *) buffer来看内存地址

      • pd2 = new (buffer) double[N];
        pd2 = new (buffer + N * sizeof(double)) double[N];
        
      • 不需要delete,因为new的不在动态存储区

    • static.cpp

      • 静态变量total只在程序运行时被设置为0,以后在两次函数调用之间,其值将保持不变,因此能够记录读取的字符总数。

      • using namespace std;
        char input[ArSize];
        char next;
        
        cout << "Enter a line:\n";
        cin.get(input, ArSize);
        while (cin)
        {
        cin.get(next);
        while (next != '\n')    // string didn't fit!
        cin.get(next);      // dispose of remainder
        strcount(input);
        cout << "Enter next line (empty line to quit):\n";
        cin.get(input, ArSize); 
        }
        cout << "Bye\n";
        
    • tip: C++也为静态存储持续性变量提供了3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)

    • 补充:cv限定符,cv表示const和volatile

      • 在C++看来,全局const定义(如下述代码段所示)就像使用了static说明符一样。
      • const int finger = 10; same as ------ static const int finger = 10;
  10. 第十章代码

    • stock.h stock.cpp

      • 通过内联函数来使得不需要重复输入计算代码

      • Stock();        // default constructor
        Stock(const std::string & co, long n = 0, double pr = 0.0);
        ~Stock();       // noisy destructor
        
      • 默认构造函数

        company = "no name";
        shares = 0;
        share_val = 0.0;
        total_val = 0.0;
        
      • this是个对象指针,返回对象,要加*

      • 使用对象指针的函数:&top->topval(stocks[st])

    • 作用域为类的常量

      • 一种方式是在类中声明一个枚举

        用这种方式声明枚举并不会创建类数据成员。也就是说,所有对象中都不包含枚举。另外,Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用30来替换它

      • 另一种在类中定义常量的方式——使用关键字static

        static const int Months = 12;

  11. 第十一章代码

    • 运算符重载函数

      • Time operator+(const Time & t) const;

      • morefixing.operator+(total);

      • total = weeding + waxing;

      • Time operator*(double n) const;
        friend Time operator*(double m, const Time & t)
        	{ return t * m; }   // inline definition
        //这样数*对象和对象*数都实现了
        
    • 友元函数

      • 通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。

      • 对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。

      • 虽然该友元函数在类声明中声明,但是它不是成员函数,因此不能使用成员运算符来调用;

      • 虽然该友元函数不是成员函数,但它与成员函数的访问权限相同

      • 编写函数定义。因为它不是成员函数,所以不要使用Time::限定符。另外,不要在定义中使用关键字friend

      • 加了const好像不能当左值:os << t.hours;所以std::ostream & os不能加const

      • std::ostream & operator<<(std::ostream & os, const Time & t)
        {
            os << t.hours << " hours, " << t.minutes << " minutes";
            return os; 
        }//return os 因为才能在同一行多次<< 
        
      • 如果函数功能在其他函数实现了,可以调用其他函数:

      • friend Time operator*(double m, const Time & t)
                { return t * m; } 
        
    • 转换函数1

      • 当构造函数只接受一个参数时,可以使用:Stonewt incognito = 257;
      • Stonewt incognito(257);
      • Stonewt incognito = Stonewt(257);
      • 后两种也可以接受多参数输入
      • 定义后,可以incognito = 276.8; taft = 325; same as taft = Stonewt(325);
      • void display(const Stonewt & st, int n); display(422, 2);
    • 转换函数2

      • operator int() const; operator double() const;
      • Stonewt poppins(9,2.8); 使用double p_wt = poppins; 时候会使用到
      • int (poppins)也会使用到
      • Vector Vector::operator-() const;当输入参数为0个时,为负号重载运算符
  12. 第十二章代码

    • stringbad.h stringbad.cpp vegnews.cpp

      • 它使用char指针(而不是
        char数组)来表示姓名。这意味着类声明没有为字符串本身分配存储空间,而是在构造函数中使用new来为字符串分配空间。这避免了在类声明中预先定义字符串的长度。
      • 其次,将num_strings成员声明为静态存储类。只是为了方便说明静态数据成员,并指出潜在的编程问题,字符串类通常并不需要这样的成员。
      • str = new char[len + 1];
      • delete [] str;
      • 传入const char * s,计算长度len = std::strlen(s);
      • std::strcpy(str, s);
      • 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。您可以使用这种格式来创建对象,从而分配和初始化内存。对于静态类成员,可以在类声明之外,如在stringbad.cpp而不是在头文件,使用单独的语句来进行初始
      • 字符串并不保存在对象中。字符串单独保存在堆内存中,对象仅保存了指出到哪里去查找字符串的信息。
      • 要创建字符串副本,所以不能直接str=s,防止重复释放
      • 首先,将headline2作为函数参数来传递从而导致析构函数被调用。其次,虽然按值传递可以防止原始参数被修改,但实际上函数已使原始字符串无法识别,导致显示一些非标准字符(显示的文本取决于内存中包含的内容)。
      • 主函数:创建了三个对象,一个用引用传递参数,一个用按值传递,一个复制构造函数,一个赋值运算符
      • 默认复制构造是浅拷贝,按值传递参赛后,会调用,然后函数结束会自动析构,造成问题
      • 因为自动存储对象被删除的顺序与创建顺序相反,所以最先删除的3个对象是knots、sailor和sport。
      • StringBad knot;会调用默认构造函数,也是会创建对象的
      • 在类声明初始化的静态:static const,enum
    • sting1.h string1.cpp sayings1.cpp saying2.cpp

      • String::String(const char * s)     // 构造函数
        {
            len = std::strlen(s);          // set size
            str = new char[len + 1];       // allot storage
            std::strcpy(str, s);           // initialize pointer
            num_strings++;                 // set object count
        }
        
      • String & String::operator=(const String & st) //赋值运算符
        {
            if (this == &st)
                return *this;
            delete [] str;//先删除本身
            len = st.len;
            str = new char[len + 1];
            std::strcpy(str, st.str);
            return *this;
        }
        
      • new char[1]与new char分配内存量一致,但需要兼容delete []所以使用前者

      • delete[]与空指针兼容。因此也可用str = nullptr来指向空指针

    • 返回对象将调用复制构造函数,而返回引用不会。

    • 定位new运算符

      • 将delete用于pc2和pc4时,将自动调用为pc2和pc4指向的对象调用析构函数;然而,将delete[]用于buffer时,不会为使用定位new运算符创建的对象调用析构函数。

      • pc3->~JustTesting();  // destroy object pointed to by pc3
        pc1->~JustTesting();  // destroy object pointed to by pc1
        delete [] buffer;     // free buffer
        

        对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。

        原因在于,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区。

    • C++11提供了另一种禁用方法的方式——使用关键字delete,与其将来面对无法预料的运行故障,不如得到一个易于跟踪的编译错误,指出这些方法是不可访问的。

    • C++为类构造函数提供了一种可用来初始化数据成员的特殊语法。这种语法包括冒号和由逗号分隔的初始化列表,被放在构造函数参数的右括号后,函数体的左括号之前。每一个初始化器都由被初始化的成员的名称和包含初始值的括号组成。从概念上来说,这些初始化操作是在对象创建时进行的,此时函数体中的语句还没有执行。语法如下:

      • Queue::Queue(int qs) : qsize(qs),front(NULL){}
  13. 第十三章代码

    • public继承

      • 使用公有派生,基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问
      • 派生类对象包含基类对象。
      • 派生类对象存储了基类的数据成员(派生类继承了基类的实现)、派生类对象可以使用基类的方法(派生类继承了基类的接口)。
      • 派生类构造函数必须使用基类构造函数。
      • 创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。
      • 必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数
      • 如果这个类没有使用动态内存分配,执行成员复制的隐式复制构造函数是合适的
      • 释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。
      • 基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象
      • 基类指针或引用只能用于调用基类方法,因此,不能使用rt或pt来调用派生类的ResetRanking方法
      • 使用子类来初始化基类,存在隐式复制构造函数,隐式重载赋值运算符,基类部分被复制给它
      • is-a继承关系,has-a成员关系
    • 多态共有继承

      • 希望同一个方法在派生类和基类中的行为是不同的。

      • Brass类和BrassPlus类都声明了ViewAcct( )和Withdraw( )方法,但BrassPlus对象和Brass对象的这些方法的行为是不同的;程序将使用对象类型来确定使用哪个版本:

      • Brass类在声明ViewAcct( )和Withdraw( )时使用了新关键字virtual。这些方法被称为虚方法(virtual method);对于在两个类中行为相同的方法,则只在基类中声明。

      • Brass类还声明了一个虚析构函数,虽然该析构函数不执行任何操作。

      • 使用virtual,如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。因此,经常在基类中将派生类会重新定义的方法声明为虚方法。

      • 虚析构函数:确保释放派生对象时,按正确的顺序调用析构函数,如果析构函数是虚的,将调用相应对象类型的析构函数。

      • 如果析构函数是虚的,将调用相应对象类型的析构函数。

      • 关键字virtual只用于类声明的方法原型中,而没有用于程序清单的方法定义中

      • 非构造函数不能使用成员初始化列表语法,但派生类方法可以调用公有的基类方法。

      • 如果代码没有使用作用域解析运算符,编译器将认为ViewAcct( )是BrassPlus::ViewAcct( ),这将创建一个不会终止的递归函数——这可不
        好。

      • 可以在类实现中写一些辅助函数,来防止代码重复

        typedef std::ios_base::fmtflags format;
        typedef std::streamsize precis;
        format setFormat();
        void restore(format f, precis p);
        format setFormat()
        {
            // set up ###.## format
            return cout.setf(std::ios_base::fixed, 
                        std::ios_base::floatfield);
        } 
        
        void restore(format f, precis p)
        {
            cout.setf(f, std::ios_base::floatfield);
            cout.precision(p);
        }
        
    • 静态联编和动态联编

      • 程序调用函数时,将使用哪个可执行代码块呢,在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)。虚函数使这项工作变得更困难,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)。

      • C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型;但是,指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换。

      • 按值传递导致只将BrassPlus对象的Brass部分传递给函数fv( )。但随引用和指针发生的隐式向上转换导致函数fr( )和fp( )分别为Brass对象和BrassPlus对象使用Brass::ViewAcct( )和BrassPlus::ViewAcct( )。

      • 通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地
        址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到vtbl中(参见图13.5)。注意,无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已。

      • 使用虚函数时,在内存和执行速度方面有一定的成本,包括:每个对象都将增大,增大量为存储地址的空间;对于每个类,编译器都创建一个虚函数地址表(数组);对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

      • 构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。

      • 析构函数应当是虚函数,除非类不用做基类。如果使用默认的静态联编,delete语句将调用~Employee( )析构函数。这将释放由Singer对象中的Employee部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚的,则上述代码将先调用~Singer析构函数释放由Singer组件指向的内存,然后,调用~Employee()析构函数来释放由Employee组件指向的内存。

      • 通常应给基类提供一个虚析构函数,即使它并不需要析构函数。

      • 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。

      • 新定义将showperks( )定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。总之,重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。

        也就是说:子类的virtual void showperks() const会覆盖掉virtual void showperks(int a)const

      • 但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变(covariance of return type),因为允许返回类型随类类型的变化而变化,这种例外只适用于返回值,而不适用于参数。

      • 如果基类声明被重载了,则应在派生类中重新定义所有的基类版本如果不需要修改,则新定义可只调用基类版本:

      • 所以要么如果子类和父类的方法一样,则要么不用虚函数,在父类用就行,要么就用虚函数,但是要重新定义基类版本

      • 用vitual不同输入参数,但同函数名也会覆盖

    • protected:BrassPlus类可以直接访问balance,而不需要使用Brass方法。

    • 抽象基类

      • 从Ellipse和Circle类中抽象出它们的共性,将这些特性放到一个ABC中,然后从该ABC派生出Circle和Ellipse类。这样,便可以使用基类指针数组同时管理Circle和Ellipse对象,即可以使用多态方法)。
      • 在这个例子中,这两个类的共同点是中心坐标、Move( )方法(对于这两个类是相同的)和Area( )方法(对于这两个类来说,是不同的)。确实,甚至不能在ABC中实现Area( )方法,因为它没有包含必要的数据成员。C++通过使用纯虚函数(pure virtual function)提供未实现的函数。纯虚函数声明的结尾处为=0
      • 当类声明中包含纯虚函数时,则不能创建该类的对象。这里的理念是,包含纯虚函数的类只用作基类。要成为真正的ABC,必须至少包含一个纯虚函数。
      • C++甚至允许纯虚函数有定义。例如,也许所有的基类方法都与Move( )一样,可以在基类中进行定义,但您仍需要将这个类声明为抽象的。在这种情况下,可以将原型声明为虚的:
    • 派生类不使用new

      • 是否需要为lackDMA类定义显式析构函数、复制构造函数和赋值运算符呢?不需要。
      • 派生类的默认析构函数总是要进行一些操作:执行自身的代码后调用基类析构函数
      • lacksDMA类的默认复制构造函数使用显式baseDMA复制构造函数来复制lacksDMA对象的baseDMA部分。因此,默认复制构造函数对于新的lacksDMA成员来说是合适的,同时对于继承的baseDMA对象来说也是合适的
      • 类的默认赋值运算符将自动使用基类的赋值运算符来对基类组件进行赋值。因此,默认赋值运算符也是合适的。
    • 派生类使用new

      • 派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行工作的进行清理。

      • hasDMA复制构造函数只能访问hasDMA的数据,因此它必须调用baseDMA复制构造函数来处理共享的baseDMA数据

      • hasDMA::hasDMA(const hasDMA & hs)
                 : baseDMA(hs)  // invoke base class copy constructor
        {
            style = new char[std::strlen(hs.style) + 1];
            std::strcpy(style, hs.style);
        }
        
        hasDMA::~hasDMA()
        {
            delete [] style;
        }
        
        hasDMA & hasDMA::operator=(const hasDMA & hs)
        {
            if (this == &hs)
                return *this;
            baseDMA::operator=(hs);  // copy base portion
            delete [] style;         // prepare for new style
            style = new char[std::strlen(hs.style) + 1];
            std::strcpy(style, hs.style);
            return *this;
        }
        
      • 对于友元:

        std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
        {
            os << (const baseDMA &) hs;
            os << "Style: " << hs.style << std::endl;
            return os;
        }
        
  14. 第十四章代码

    • 包含对象成员的类

      • 使用这样的类成员:本身是另一个类的对象。这种方法称为包含(containment)、组合(composition)或层次化(layering)。
      • 获得接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。
      • 将该typedef放在类定义的私有部分意味着可以在Student类的实现中使用它,但在Student类外面不能使用。
      • explicit关闭隐式转换
      • C++要求在构建对象的其他部分之前,先构建继承对象的所有成员对象。因此,如果省略初始化列表,C++将使用成员对象所属类的默认构造函数。
      • 当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。
    • 使用私有或保护继承

      • 使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。

      • 私有继承提供的特性与包含相同:获得实现,但不获得接口。

      • 新的Student类不需要私有数据,因为两个基类已经提供了所需的所有数据成员。

      • 对于继承类,新版本的构造函数将使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数:

      • 访问基类的方法:

        • ArrayDb::size() > 0 使用私有继承
        • scores.size() > 0 不使用私有继承
      • Student类的代码如何访问内部的string对象呢?

        • 使用强制类型转换

          return (const string &) *this;
          
      • 访问基类的友元函数

        • 通过显式地转换为基类来调用正确的函数

        • ostream & operator<<(ostream & os, const Student & stu)
          {
              os << "Scores for " << (const string &) stu  << ":\n";
              stu.arr_out(os);  // use private method for scores
              return os;
          }
          
    • 使用包含还是私有继承

      • 继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如包含同名方法的独立的基类或共享祖先的独立基类。
      • 包含能够包括多个同类的子对象。如果某个类需要3个string对象,可以使用包含声明3个独立的string成员。而继承则只能使用一个这样的对象(当对象都没有名称时,将难以区分)。
      • 私有继承所提供的特性确实比包含多,通过继承得到的将是派生类,因此它
        能够访问保护成员。
      • 另一种需要使用私有继承的情况是需要重新定义虚函数。派生类可以重新定义虚函数,但包含类不能。使用私有继承,重新定义的函数将只能在类中使用,而不是公有的。
      • 公有继承可以隐式向上转换,私有继承不可以隐式向上转换
      • 保护继承只能在派生类中隐式向上转换,不能在外面
      • 使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有派生。using声明只使用成员名——没有圆括号、函数特征标和返回类型。
    • 多重继承使得能够使用两个或更多的基类派生出新的类,将基类的功能组合在一起

      • 如果添加一个从Singer和Waiter类派生出的SingingWaiter类后,将带来一些问题。具体地说,将出现以下问题。有多少Worker ,哪个方法

      • 继承两个基类对象,包含两个Worker对象拷贝

      • C++引入多重继承的同时,引入了一种新技术——虚基类(virtual base class),使MI成为可能

      • 虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。

      • 通过在类声明中使用关键字virtual,可以使Worker被用作Singer和Waiter的虚基类

      • 现在,SingingWaiter对象将只包含Worker对象的一个副本。从本质上说,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本。因为SingingWaiter现在只包含了一个Worker子对象,所以可以使用多态。

      • 使用虚基类时,需要对类构造函数采用一种新的方法。对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数。

        SingingWaiter(const Worker & wk, int p = 0, int v = other)
            : Worker(wk), Waiter(wk,p), Singer(wk,v) {}
        
      • 对于哪个方法的问题,可以使用对象.Singer::Show().

      • Singer::show();

      • 使用虚基类将改变C++解析二义性的方式。使用非虚基类时,规则很简单。如果类从不同的类那里继承了两个或更多的同名成员(数据或方法),则使用该成员名时,如果没有用类名进行限定,将导致二义性。但如果使用的是虚基类,则这样做不一定会导致二义性。在这种情况下,如果某个名称优先于(dominates)其他所有名称,则使用它时,即便不使用限定符,也不会导致二义性。派生类中的名称优先于直接或间接祖先类中的相同名称。任何一个omg( )定义都不优先于其他omg( )定义,因为C和E都不是对方的基类。所以,在F中使用非限定的omg( )将导致二义性。

    • 类模板

      • 不能将模板成员函数放在独立的实现文件中,模板必须与特定的模板实例化请求一起使用。

      • template <class T, int n>
        class ArrayTP
        {
        private:
            T ar[n];
        public:
            ArrayTP() {};
            explicit ArrayTP(const T & v);
            virtual T & operator[](int i);
            virtual T operator[](int i) const;
        };
        
      • 递归使用模板 ArrayTP<ArrayTP<int,5>,10> twodee

      • 使用多个类型参数 Pair<string, int>

      • 默认类型模板参数

    • 模板的具体化

      • 隐式实例化
      • 显式实例化
      • 显式具体化
      • 部分具体化
    • 成员模板

      • 模板可用作结构、类或模板类的成员

      • template <typename T>
        class beta
        {
        private:
            template <typename V>  // nested template class member
            class hold
            {
            private:
                V val;
        
      • 嵌套模板

    • 将模板作为参数

      • template <template class Thing>
      • Crab nebula;
    • 模板类和友元

      • 非模板友元函数与模板类
      • 模板友元函数与模板类
  15. 第十五章代码

    • 友元类

      • class Tv
        {
        public:
            friend class Remote;  
        
    • 友元类方法

      • class Tv;                       // forward declaration
        
        class Remote
        {
        
      • 事实上,唯一直接访问Tv成员的Remote方法是Remote::set_chan( ),因此它是唯一
        需要作为友元的方法。确实可以选择仅让特定的类成员成为另一个类的友元,而不必让整个类成为友元,但这样做稍微有点麻烦,必须小心排列各种声明和定义的顺序。

      • 在编译器在Tv类的声明中看到Remote的一个方法被声明为Tv类的友元之前,应该先看到Remote类的声明和set_chan( )方法的声明。

    • 嵌套类

      • 在C++中,可以将类声明放在另一个类中。在另一个类中声明的类被称为嵌套类(nested class),它通过提供新的类型类作用域来避免名称混乱。包含类的成员函数可以创建和使用被嵌套类的对象;
    • 引发异常、try块和catch块

    • 异常类

    • 运行阶段类型识别(RTTI)

    • dynamic_cast和typeid

    • static_cast、const_cast和reiterpret_cast

 类似资料: