Primer C++第五版 读书笔记(一)
Primer C++第五版 读书笔记(一) (如有侵权请通知本人,将第一时间删文) 1.1-2.2 章节 关于C++变量初始化: 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,以一个新值来替代. 定义一个名为a的int变量并初始化为0,有以下4种方法: int a = 0; int a = {0}; // 列表初始化 int a{0}; // 列表初始化 int a(0); 当列表初始化方法用于内置类型的变量时,如果初始值存在信息丢失的风险,则编译器将报错. 示例: long double ld = 3.1415926536; int a{ ld }, b{ ld }; // error C2397 从"long double"转换到"int"需要收缩转换 int c(ld), d = ld; // warning C4244: "初始化": 从"long double"转换到"int",可能丢失数据 定义变量的错误示范: double salary = wage = 9999.99; // error:没定义变量 wage std::cin >> int input_value; // 不允许 下列变量的初始值是什么? string global_str; // 全局变量,值为"" int global_int; // 全局变量,值为 0 int main() { int local_int; // 局部变量,是未定义状态,无意义数据 string local_str; // 哪怕它是局部变量,它也是string定义的,值为"" } 未初始化的变量可能引发运行时的故障. 建议初始化每一个内置类型的变量.虽然并非必须,但如果我们不能确保初始化后程序安全,那么这么做不失为一种简单可靠的方法. 注意: 变量只可以被定义一次,但可被声明多次. 如果想声明一外变量而非定义它,可在变量名前添加关键字 extern , 而不要显示地初始化变量,示例如下: extern int i; // 声明变量 i 而非定义它 int j; // 声明并定义变量 j 任何包含了显示初始化的声明即成为定义.我们能给由extern关键字标记的变量赋一个初始值,但这么做就抵消了extern的作用. extern语句如果包含了初始值就不再是声明,而变成了定义: extern double pi = 3.1415; // 定义 在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误. ============================================================================================ 2.3 C++复合类型: 引用: 引用(reference)为对象起了另外一个名字,引用类型引用(refers to)另外一种类型. int ival = 1024; int &refVal = ival; // refVal指向ival(是ival的另一个名字) int &refVal2; // 错误!!引用必须被初始化 引用即别名:引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字. 为引用赋值,实际上是把值赋给了与引用绑定的对象.获取引用的值,实际上是获取了与引用绑定的对象的值. 同理,经引用作为初始值,实际上是以与引用绑定的对象作为初始值. 因为引用本身不是一个对象,所以不能定义引用的引用. 绝大多数情况 下,引用的类型都要和与之绑定的对象严格匹配.而且,引用只能绑定到对象上,而不能与字面值或某个表达式的计算结果绑定在一起. int &refVal4 = 10; // 错误:引用类型的初始值必须是一个对象. const int &refVal4 = 10; // 正确 double dval = 3.14; int &reVal5 = dval; // 错误:引用类型要与与之绑定的对象严格匹配! 指针: 指针(pointer)是指向另外一种类型的复合类型. 指针与引用相比有很多不同点(重点): 0.引用是已经存在的对象的另一个名称,而指针是一个对象,它遵循自己的使用规则. 1.指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象. 2.指针无须在定义时赋初值,和其他内置类型一样,在块作用域内定义的指针,如果没有被初始化,也将拥有一个不确定的值.引用在定义时就必须初始化. 3.最大不同:引用本身并非一个对象,一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象. int *ip1, *ip2; // ip1 和 ip2 都是指向 int 型对象的指针 double dp, *dp2; // dp2 是指向 double 型对象的指针,dp 是 double 型对象. 获取对象的地址: 指针存放某个对象的地址,想要获取该地址,需要使用取地址符(&): int ival = 42; int *p = &ival; // p 存放变量ival的地址,或者说p是指向变量ival的指针. 示例: double dval; double *a = &dval; // 正确:初始值是double型对象的地址 double *a2 = a; // 正确:初始值是指向double开进对象的地址 int * b = a; // 错误:指针b的类型与指针a的类型不匹配! int * b2 = &vdal; // 错误:试图把double型对象的地址赋给int型指针 因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配.如果指针指向了一个其他类型的对象,对该对象的操作将发生错误. 指针值: 指针的值(即地址)应属下列4种状态之一: 1.指向一个对象 2.指向紧邻对象所占空间的下一个位置 3.空指针,意味着指针没有指向任何对象 4.无效指针,也就是上述情况之外的其他值. 试图拷贝或以其他方式访问无效指针的值都将引发错误.编译器并不负责检查此类错误,这一点和试图使用未经初始化的变量一样.因此程序员必须清楚任意给定的指针是否有效. 利用指针访问对象: 如果指针指向了一个对象,则允许使用解引用符(*)来访问该对象: int ival = 42; int *p = &ival; // p存放着变量ival的地址,或者说p是指向变量ival的指针. cout << *p; // 由符号*得到指针p所指向的对象,输出42. 对指针解引用会得到所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值: *p = 0; cout<<*p; // 输出0. 解引用操作仅适用于那些确实指向了某个对象的有效指针. 空指针: 空指针不指向任何对象,在试图使用一个指针之前可先检查它是否为空,以下列出几个生成空指针的方法: int *p1 = nullptr; // 等价于 int *p1 = 0; 这是C++11新标准.nullptr是一种特殊类型的字面值,它可以被转化成任意其他的指针类型. int *p2 = 0; // 直接将p2初始化为字面常量0. // 下面方法需要首先#include cstdlib int *p3 = NULL; // 等价于 int *p3 = 0; 建议:初始化所有的指针!!! 建议初始化所有的指针,并且在可能的情况下,尽量等定义了对象之后再定义指向它的指针.如果实在不清楚指针会指向何处, 就把它初始化为nullptr或者0,这样程序就能检测并知道它没有指向任何具体的对象了. 任何非0指针对应的条件值都是true.示例如下: int ival = 1024; int *pi = 0; int *pi2 = &ival; if(pi) // false if(pi2) // true void* 指针 void* 是一种特殊的指针类型,可用于存放任意对象的地址.我们对该地址中到底是什么类型的对象不清楚. 概括来说,以 void* 的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象. 问题:给定指针p,你能知道它是否指向了一个合法的对象吗?如果能?叙述判断思路,如果不能,说明原因. 答:不能,因为需要更多的信息来确定该指针是否有效. 定义多个变量: int* p; // 合法但容易产生误导 int *p1, p2; // p1是指向int的指针,p2是int类型 int *p1, *p2; // p1和p2都是指向int的指针(本书推荐) 指向指针的指针: int ival = 1024; int *pi = &ival; // pi指向一个int型的数 int **ppi = π // ppi指向了指针pi的地址 示例: int main() { int ival = 1024; int *pi = &ival; // pi指向一个int型的数 int **ppi = π // ppi指向了指针pi的地址 cout << "ival = " << ival << endl; // ival 1024 cout << "*pi = " << *pi << endl; // *pi = 1024 cout << "**ppi = " << **ppi << endl; // **ppi = 1024 cout << "pi = " << pi << endl; // pi = 000000F3B439F8B4 cout << "*ppi = " << *ppi << endl; // *ppi = 000000F3B439F8B4 cout << (*ppi == pi) << endl; // 1 return 0; } 指向指针的引用(难点): 引用本身不是一个对象,因此不能定义指向引用的指针.但指针是对象,所以存在对指针的引用. 示例如下: int main() // 这个示例的关键是p,r都存的是i的地址(即&i). { int i = 42; int *p = &i; // p是一个int型指针,它指向i,是变量i的地址. int *&r = p; // r是一个对指针p的引用 cout << "&r = " << &r << endl; // &r = 00000081A017FB18 cout << "&i == p == r 吗?下面开始打印: " << endl; cout << "&i = " << &i << endl; // &i = 00000081A017FAF4 cout << "p = " << p << endl; // p = 00000081A017FAF4 cout << "r = " << r << endl; // r = 00000081A017FAF4 *r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0. cout << "*r = 0之后 i = " << i << endl; // *r = 0之后 i = 0 system("pause"); return 0; } ============================================================================================ 2.4 const限定符 const是一种类型修饰符,用于说明永不改变的对象.const对象一旦定义就无法再赋新值,所以必须初始化. const int bufSize = 512; //输入缓冲区大小 这样定义就把bufSize定义成了一个常量,任何试图为bufSize赋值的行为都将发生错误. 因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化. 默认状态下,const对象仅在文件内有效. 如果想只在一个文件中定义const,而在其他多个文件中声明并使用它,需要做如下操作: 对于const变量不管是声明还是定义都添加extern关键字,这样只需要定义一次就可以了. 注意:如果想在多个文件之间共享const对象,必须在变量的定义前添加extern关键字. const指针: 常量指针(const pointer)是一种指针,它的值永不改变. 允许把指针本身定为常量.常量指针(const pointer)必须初始化,而且一旦初始化,它的值(也就是存放在指针中的那个地址) 就不能改变了.把*放在const关键字之前用以说明指针是一个常量. 下面的定义声明哪些合法哪些不合法? int i, *const cp; // 不合法,cp必须被初始化 int *p1, *const p2; // 不合法,p2必须被初始化 const int ic, &r = ic; // 不合法,ic必须被初始化 const int *const p3; // 不合法,p3必须被初始化 const int *p; // 合法,p指针指向一个const int类型的数据 顶层const: 顶层const(top-level const)表示***指针本身是一个常量***,而底层const(low-level const)表示***指针所指的对象是一个常量***. 更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用.如算术类型,类,指针等. 底层const则与指针和引用等复合类型的基本类型部分有关.比较特殊的是,指针类型既可是顶层const,也可是底层const,这一点和其他类型区别明显: int i = 0; int *const p1 = &ri; // 不能改变p1的值,这是一个顶层const const int ci = 42; // 不能改变ci的值,这是一个顶层const const int *p2 = &ci; // 允许改变p2的值,这是一个底层const const int *const p3 = p2; // 靠右的const是顶层const,靠左的是底层const const int &r = ci; // 用于声明引用的const都是底层const 当执行拷贝时,常量是顶层const还是底层const区别明显.其中顶层const不受什么影响: i = ci; // 正确:拷贝ci的值,ci是一个顶层cosnt,对此操作无影响 p2 = p3; // 正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响. 执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响. 另一方面,底层const的限制却不能忽视.当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能转换. 一般来说,非常量可以转换成常量,反之则不行. int *p = p3; // 错误:p3包含底层const的定义,而p没有. p2 = p3; // 正确:p2和p3都是底层const p2 = &i; // 正确:int *能转换成const int * int &r = ci; // 错误,普通的int&不能绑定到int常量上 const int &r2 = i; // 正确:const int& 可以绑定到一个普通int上. p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但必须清楚它指向的对象得是一个常量.因此,不能用p3去初始化p, 因为p指向的是一个普通的(非常量)整数.另一方面,p3的值可以赋值给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const), 仅就这次赋值不会有什么影响. constexpr(const expression)和常量表达式: 常量表达式(const expression)是指值不会改变并且在编译过程中就能计算结果的表达式. const int max_files = 20; // max_files是常量表达式 const int limit = max_files+1; // limit是常量表达式 int staff_size = 27; // staff_size不是常量表达式,因为staff_size可能会被赋予其它值 const int sz = get_size(); // sz不是常量表达式,因为编译过程中看不出get_size()是多少. constexpr变量: C++11新标准规定:允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式. 声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化: constexpr int mf = 20; // 20是常量表达式 constexpr int limit = mf +1; // mf+1是常量表达式 constexpr int sz = size(); // 只有当size是一个constexpr函数时才是一条正确的声明语句. 新标准允许定义一种特殊的constexpr函数,这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了. 一般来说,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型. 指针和constexpr: 必须明确一点:在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关: constexpr int *p = nullptr; // p是一个指向整数的常量指针.constexpr把它所定义的对象置为了顶层const. 与其他常量指针类似,constexpr指针既可以指向常量也可指向一个非常量; constexpr int *np = nullptr; // np是一个指向整数的常量指针,其值为空 int j = 0; constexpr int i = 42; // i的类型是整型常量 // i和j都必须定义在函数体之外 constexpr const int *p = &i; // p是常量指针,指向整型常量i constexpr int *p1 = &j; // p1是常量指针,指向整数j 2.5 处理类型: 类型别名: 有两种方法可用于定义类型别名,传统的方法是使用关键字typedef: typedef double wages; // wages是double的同义词 typedef wages base, *p; // base是double的同义词,p是double*的同一词 新标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名: using SI = Sales_item; // SI是Sales_item的同义词 指针,常量和类型别名: typedef char *pstring; // pstring是类型char *的别名 const pstring cstr = 0; // cstr是指向char的常量指针 const pstring *ps; // ps是一个指针,它的对象是指向char的常量指针 const pstring是指向char的常量指针,并非指向常量字符的指针. auto类型说明符: auto定义的变量必须有初始值. // 由val1和val2相加的结果可以推断出item的类型 auto item = val1 + val2; // item初始化为val1和val2相加的结果 使用auto同时声明多个变量时,该语句中的所有初始基本数据类型都必须一样: auto i = 0, *p = &i; // 正确:i是整数,p是整型指针 auto sz = 0, pi = 3.14; // 错误:sz和pi的类型不一致 复合类型,常量和auto: 编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则. 首先,正如我们所熟知的,使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值. 此时编译器以引用对象的类型作为auto类型. int i = 0, &r = i; auto a = r; // a是一个整数(r是i的别名,而i是一个整数) 其次,auto一般会忽略掉顶层const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时: const int ci = i, &cr = ci; auto b = ci; // b是一个整数(ci的顶层const特性被忽略掉了) auto c = cr; // c是一个整数(cr是ci的别名,ci本身是一个顶层const) auto d = &i; // d是一个整型指针(整数的地址就是指向整数的指针) auto e = &ci; // e是一个指向整数常量的指针(对常量对象取地址是一种底层const) 如果希望推断出的auto类型是一个顶层const,需要明确指出: const auto f = ci; // ci的推演类型是int,f是const int; 还可以将引用的类型设置为auto,此时原来的初始化规则仍然适用: auto &g = ci; // g是一个整型常量引用,绑定到ci auto &h = 42; // 错误,不能为非常量引用绑定字面值 const auto &j = 42; // 正确,可以为常量引用绑定字面值 设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留,和往常一样,如果我们给初始值绑定一个引用,则此时的常量就不是顶层常量了. 要在一条语句中定义多个变量,切记,符号&和*只从属于某个声明符,而 非基本数据类型的一部分,因此初始值必须是同一种类型: auto k = ci, &l = i; // k是整数,l是整型引用 auto &m = ci, *p = &ci; // m是对整型常量的引用,p是指向整型常量的指针 auto &n = i, *p2 = &ci; // 错误:i的类型是int而&ci的类型是const int 示例: int main() { int i = 0, &r = i; auto a = r; // int a = 0; const int ci = i, &cr = ci; auto b = ci; // int b = 0; auto c = cr; // int c = 0; auto d = &i; // int *d = &i; auto e = &ci; // const int *e = &ci; const auto f = ci; // const int f = 0; auto &g = ci; // const int &g = 0; system("pause"); return 0; } decltype类型指示符: decltype作用是选择并返回操作数的数据类型.在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值: decltype(f()) sum = x; // sum 的类型就是函数f的返回类型 编译器并不实际调用函数f,而是使用当调用发生时f的返回值类型作为sum的类型.换句话说,编译器为sum指定的类型就是 假如f被调用的话就会返回的那个类型. decltype处理顶层const和引用的方式与auto有些许不同.如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用在内): const int ci = 0, &cj = ci; decltype(ci) x = 0; // x的类型是const int decltype(cj) y = x; // y的类型是const int &, y绑定到变量x decltype(cj) z; // 错误:z是一个引用,必须初始化 因为cj是一个引用,decltype(cj)的结果就是引用类型,因此作为引用的z必须被初始化. 需要指出的是,引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外. decltype和引用: 如果decltype使用的表达式不是一个变量,则decltype返回表达式结果对应的类型. 有些表达式将向decltype返回一个引用类型,一般来说当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值: // decltype的结果可以是引用类型 int i = 42, *p = &i, &r = i; decltype(r+0) b; // 正确,加法的结果是int,因此b是一个(未初始化的)int decltype(*p) c; // 错误,c是int&,必须初始化. 如果表达式的内容是解引用操作,则decltype将得到引用类型. decltype和auto的另一处重要区别是:decltype的结果类型与表达式形式密切相关.有一种情况特别要注意:对于decltype所用的表达式来说, 如果变量名加上了一对括号,则得到的类型与不加括号会有不同.如果decltype使用的是一个不加括号的变量, 则得到的结果就是该变量的类型,如果给变量加上一层或多层括号,编译器就会把它当作是一个表达式. 变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型: // decltype的表达式如果是加上了括号的变量,结果将是引用 decltype((i)) d; // 错误,d是int&,必须初始化; decltype(i) e; // 正确,e是一个(未初始化的)int 切记:decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)的结果只有当variable本身就是一个引用时才是引用. 示例: 1.关于下面的代码,指出每个变量的类型及程序结束时它们各自的值: int a = 3, b = 4; decltype(a) c = a; decltype((b)) d = a; ++c; ++d; c的类型是int,d的类型是int&,c与d的最终值都为4. 赋值是会产生引用的一类典型表达式,引用的类型就是左值的类型.也就是说,如果i是int,则表达式i=x的类型就是int&. 2.指出下面的代码中每一个变量的类型和值: int a = 3, b = 4; decltype(a) c = a; // c是int型. decltype(a = b) d = a; // d是int&类型 3.auto指定类型与decltype指定类型一样和不一样的示例: int i = 0, &r = i; // 一样 auto a = i; // a是int型 decltype(i) b = i; // b是int型 // 不一样 auto c = r; // c是int型 decltype(r) d = r; // d是int&类型 2.6 自定义数据类型 自定义数据类以关键字struct开始,紧跟着类名和类体(其中类体部分可以为空).类体由花括号包围形成一个新的作用域. 类内部定义的名字必须唯一,但可以与类外部定义的名字重复. struct Sales_data{ /*...*/ } accum, trans, *salesptr; // 与上一条语句等价,但可能更好一些 struct Sales_data { /*...*/ }; Sales_data accum, trans, *salesptr; 一般来说,最好不要把对象的定义和类的定义放在一起,这么做无异于把两种不同实体的定义混在了一条语句里.(不建议) 类数据成员: 类我是个定义类的成员,我们的类只有数据成员(data member),类的数据成员定义了类的对象的具体内容,每个对象有自己的一份 数据成员拷贝.修改一个对象的数据成员,不会影响其他的对象. 定义数据成员的方法和定义普通变量一样:首先说明一个基本数据类型,随后紧跟一个或多个声明符. C++11新标准规定,可以为数据成员提供一个类内初始值(in-class initializer).创建对象进,类内初始值将用于初始化数据成员. 没有初始值的成员被默认初始化. 编写自己的头文件: 类一般都不定义在函数体内,当在函数体外部定义类时,在各个指定的源文件中可能只有一处该类的定义.而且,如果要在不同文件中使用 同一个类,类的定义就必须保持一致. 为了确保各个文件中类的定义的一致性,类通常被定义在头文件中,而且类所在头文件的名字应与类的名字一样.例如我们应该把Sales_data类定义在名为Sales_data.h的头文件中. 头文件通常包含那些只能被定义一次的实体,如类,const和constexpr变量等.头文件也经常用到其他头文件的功能. 注意:头文件一旦改变,相关的源文件必须重新编译以获取更新过的声明. 预处理器概述: 确保头文件多次包含仍能安全工作的常用技术是预处理器(preprocessor),它由C++语言从C语言继承而来.预处理器在编译之前执行一段程序, 可以部分地改变我们所写的程序.之前已经用到了一项预处理功能#include. 当预处理器看到#include标记时就会用指定的头文件的内容代替#include. C++程序还会用到的一项预处理功能就是头文件保护符(header guard),头文件保护符依赖于预处理变量.预处理变量有两种状态:已定义和未定义. #define指令把一个名字设定为预处理变量,另外两个指令则分别检查某个指定的预处理变是否已定义: #ifdef:当且仅当变量已定义时为真, #ifndef:当且仅当变量未定义时为真. 一旦检查结果为真,则执行后续操作直至遇到#endif指令为止. 使用这些功能就能有效地防止重复包含的发生: #ifndef SALES_DATA_H #define SALES_DATA_H #include<string> struct Sale_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; }; #endif 第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作直至遇到#endif为止. 此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来.后面如果再一次包含Sales_data.h, 则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分. 警告:预处理变量无视C++语言中关于作用域的规则. 整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性. 为了与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写. 头文件即使(目前还)没有被包含在任何其他头文件中,也应该设置保护符.头文件保护符很简单,程序员只要习惯性地加上就可以了,没必要太在乎你的程序到底需不需要. 自定义头文件,数据类型示例: 1.在当前项目的"头文件"文件夹内新建 Sales_data.h 文件,内容如下: #ifndef CH02_EX2_42_H_ #define CH02_EX2_42_H_ #include <string> #include <iostream> struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0; void CalcRevenue(double price); double CalcAveragePrice(); void SetData(Sales_data data); void AddData(Sales_data data); void Print(); }; #endif 2.在当前项目的"源文件"文件夹内新建 Sales_data.cpp 文件,内容如下: #include<iostream> #include "Sales_data.h" void Sales_data::CalcRevenue(double price) { revenue = units_sold * price; std::cout << "CalcRevenue正在执行..." << "revenue = " << revenue << std::endl; } void Sales_data::SetData(Sales_data data) { bookNo = data.bookNo; units_sold = data.units_sold; revenue = data.revenue; std::cout << "SetData执行完毕..." << std::endl; } void Sales_data::AddData(Sales_data data) { if (bookNo != data.bookNo) return; units_sold += data.units_sold; revenue += data.revenue; std::cout << "AddData正在执行..." << std::endl; std::cout << "units_sold的值现在是: " << units_sold << std::endl; std::cout << "revenue的值现在是: " << revenue << std::endl; } double Sales_data::CalcAveragePrice() { if (units_sold != 0) { std::cout << "CalcAveragePrice正在执行..." << "AveragePrice的值为: " << revenue / units_sold << std::endl; return revenue / units_sold; } else return 0.0; } void Sales_data::Print() { std::cout << "Print正在执行..." << "bookNo = " << bookNo << ", " << "units_sold = " << units_sold << ", " << "revenue = " << revenue << std::endl; double averagePrice = CalcAveragePrice(); if (averagePrice != 0.0) std::cout << "averagePrice = " << averagePrice << std::endl; else std::cout << "(no sales)" << std::endl; } 3.在当前项目的"源文件"文件夹内新建 main.cpp 文件,内容如下: #include<iostream> #include"Sales_data.h" using namespace std; #endif int main() { Sales_data total; double totalPrice; std::cout << "请依次输入bookNo, units_sold, totalPrice这三个变量,以空格作为分隔符,以回车符结束: " << std::endl; if (std::cin >> total.bookNo >> total.units_sold >> totalPrice) { total.CalcRevenue(totalPrice); Sales_data trans; double transPrice; while (std::cin >> trans.bookNo >> trans.units_sold >> transPrice) { trans.CalcRevenue(transPrice); if (total.bookNo == trans.bookNo) { total.AddData(trans); } else { total.Print(); total.SetData(trans); } } total.Print(); return 0; } else { std::cerr << "No data?!" << std::endl; return -1; // indicate failure } system("pause"); return 0; } 编译执行即可. ============================================================================================