本文是通过第五版《C++ primer》进行的查漏补缺。
输入输出 标准库定义了4个IO对象:
cin : istream类型的对象,标准输入(standard input)
cout : ostream类型的对象,标准输出(standard output)
cerr : ostream类型的对象,标准错误(standard error)
clog : ostream类型的对象,用来输出程序运行时的一般信息
一种不用namespace std的写法:
1 2 3 4 5 6 7 8 9 10 11 #include <iostream> int main () { std::cout<<"enter two numbers:" <<std::endl; int v1=0 , v2=0 ; std::cin>>v1>>v2; std::cout<<"the sum of " <<v1<<" and " <<v2<<" is " <<v1+v2<<std::endl; return 0 ; }
std::cout 两个冒号是一个运算符,作用域运算符。它表示我要把std作用域里面的cout拿出来用。
endl 操作符,结束当前行,将设备相关的缓冲区内容刷到屏幕上。
cin可以跳过空格、制表符、换行符等空白字符。
一般来说,自己创建的头文件,用双引号。
1 2 3 4 5 6 7 8 9 10 11 12 #include <iostream> int main () { int sum=0 , value=0 ; while (std::cin>>value){ sum+=value; } std::cout<<"the sum is " <<sum<<std::endl; return 0 ; }
在上例中,无效的istream对象会使条件变为假,例如输入一个字母(非int型)。
基本内置类型 char比较特殊,分为三种:char、signed char、unsigned char,char是signed char或unsigned char其中的一种,由编译器决定:VC编译器、x86上的GCC都把char定义为signed char,而arm-linux-gcc把char定义为unsigned char。
为了保持程序的移植性,应当明确指出到底是哪一种。
三者都占1个字节。signed char取值范围是-128~127(有符号位),unsigned char取值范围是0~255。
原始的ASCII标准里,定义的字符码值是只有0~127,所以怎么定义的char都刚好装得下。
变量 记一种写法:
1 2 3 4 5 6 7 8 9 10 #include <iostream> int main () { std::string book ("newbrush" ) ; std::cout<<book<<std::endl; return 0 ; }
初始化:
1 2 3 4 int units_sold = 0 ; int units_sold = {0 }; int units_sold{0 }; int units_sold (0 ) ;
若使用列表初始化,且初始值存在丢失信息的风险,则编译器报错:
1 2 3 long double ld=3.1415926536 ;int a{ld},b={ld}; int c (ld) ,d =ld;
若只声明而不定义,就在变量前添加extern关键字,且不要显式地初始化变量:
若不希望别的文件通过extern引用,可以使用static,这样作用域就是本文件。
总结: extern不是定义,是引入(声明)在其他源文件中定义的非static全局变量。
名字的作用域(scope):
同一个名字出现在程序的不同位置,也可能指向不同的实体。
C++中大多数作用域都以花括号分隔。
名字的有效区域始于名字的声明语句,以声明语句所在的作用域末端作为结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> int reused = 42 ; int main () { int unik = 0 ; std::cout<<reused<<" " <<unik<<std::endl; int reused = 0 ; std::cout<<reused<<" " <<unik<<std::endl; std::cout<<::reused<<" " <<unik<<std::endl; return 0 ; }
复合类型(compound type) 引用(reference),为对象起的别名:
1 2 3 int ival = 1024 ;int &refVal = ival; int &refVal2;
定义引用时,把引用和它的初始值绑定在一起,而不是把初始值拷贝给引用。引用不是对象,所以不能定义引用的引用,不能定义指向引用的指针。
1 2 3 reVal = 2 ; int li = refVal; int &refVal3 = refVal;
可以使用取地址符&获取指针所封装的地址:
1 2 3 int ival = 42 ;int *p = &ival; double *dp = &ival;
对于“指针的值+1”的解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> int main () { std::cout<<"hello this is a test" <<std::endl; int a = 233 ; int *p = &a; std::cout<<"adress: " <<p<<std::endl; std::cout<<"neirong: " <<*p<<std::endl; std::cout<<"adress+1: " <<p+1 <<std::endl; return 0 ; }
可以使用解引用符*利用指针访问对象:
1 2 3 4 5 int ival = 42 ;int *p = &ival; std::cout<<*p; *p=0 ; std::cout<<*p
空指针(null pointer),不指向任何对象。在使用一个指针前,可以先检查它是否为空。
1 2 3 4 5 int *p1 = nullptr ; int *p2 = 0 ;int *p3 = NULL ; int zero = 0 ;p1 = zero;
void *指针,纯粹的地址封装,与类型无关。可以用于存放任意对象的地址:
1 2 3 double obj = 3.14 , *pd = &obj;void *pv = &obj;pv = pd;
指向指针的指针:
1 2 3 int ival = 1024 ;int *pi = &ival;int **ppi = π
指针的引用:
1 2 3 4 5 int i = 1024 ;int *p;int *&r = p; r = &i; *r = 0 ;
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> using namespace std;int main () { int i = 1024 ; int * p; int *& r = p; r = &i; cout << *r << " " << *p ; return 0 ; }
const限定符 const对象必须初始化:
1 2 3 const int i = get_size (); const int j = 42 ; const int k;
默认状态下,const对象仅在文件内有效,若想在多个文件间共享const对象,必须在变量的定义之前添加关键字extern
1 2 3 4 extern const int bufSize = fcn ();extern const int bufSize;
const的引用,对常量的引用:
1 2 3 const int ci = 1024 ;const int &r1 = ci; int &r2 = ci;
一个奇怪的例子:
1 2 3 double dval = 3.14 ;const int &ri = dval; int &ri = dval;
指向常量的指针:
1 2 3 4 5 6 const double pi = 3.14 ;double *ptr = π const double *cptr = π*cptr = 42 ; double dval = 3.14 ;cptr = &dval;
const指针(必须初始化):不变的是指针本身的值,而不是它指向的那个值。
1 2 3 4 5 6 7 8 9 10 11 int errNumb = 0 ;int *const curErr = &errNumb; const double pi = 3.14159 ;const double *const pip = π if (*curErr){ errorHandler (); *curErr = 0 ; } *pip = 2.71 ;
顶层const:表示变量本身是一个常量。底层const:表示指针所指向的对象是一个const。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int i = 0 ;int *const p1 = &i; const int ci = 42 ; const int *p2 = &ci; const int *const p3 = p2; p2 = p3; int *p = p3; p2 = &i;
constexpr变量(C++11标准):允许将变量声明为constexpr类型,以便由编译器来验证变量的值是否是一个常量表达式。
一定是一个常量
必须用常量表达式初始化
自定义类型、IO库、string等类型不能被定义为constexpr
1 2 3 constexpr int mf = 20 ;constexpr int limit = mf + 1 ;constexpr int sz = size ();
指针和constexpr:限定符仅对指针有效,对其所指的对象无关。(对顶层有效,底层无效)
1 2 3 4 5 6 7 constexpr int *np = nullptr ; int j = 0 ;constexpr int i = 42 ;...... constexpr const int *p = &i; constexpr int *p1 = &j;
typedef、auto、decltype 类型别名,提高可读性:
1 2 3 typedef double wages;typedef wages base, *p; using SI = Sales_item;
对于指针,类型别名的使用可能会产生意想不到的结果(平时不用就好了):
1 2 3 4 5 typedef char *pstring;const pstring cstr = 0 ; const pstring *ps; const char *cstr = 0 ;
auto类型说明符,C++11,让编译器通过初始值推断变量的类型:
1 2 auto i = 0 , *p = &i; auto sz = 0 , pi = 3.14 ;
看看就好,不要较真,我觉得一般不会用到这些:
decltype类型说明符,获取表达式的类型。在编译时推导出一个表达式的类型,并且不会计算表达式的值。例如:
1 2 3 4 5 6 7 8 9 10 11 12 int x = 0 ;decltype (x) y = 1 ; decltype (x+y) z = x + y; const int & i = x;decltype (i) j = y; const decltype (z) *p = &z; decltype (z) *pi = &z; decltype (pi) *pp = π decltype (f ()) sum = x;
auto与decltype类似但是又不同,auto只能根据变量的初始化表达式推导出变量应该具有的类型。decltype将精确的推导出表达式定义的类型,不会舍弃和弃用cv限定符。
一个例子:
1 2 3 4 5 6 int i = 42 , *p = &i, &r = i;decltype (*p) c; decltype (i) e; decltype ((i)) d; decltype (((i))) d1 = i;
自定义数据结构、类和头文件 类定义可以使用关键字class或struct,二者默认的继承、访问权限不同,struct是public的,class是private的。
编写自己的头文件:
1 2 3 4 5 6 7 8 9 10 11 12 #ifndef SALES_DATA_H #define SALES_DATA_H #include <string> struct Sales_data { std::string bookNo; unsigned units_sold = 0 ; double revenue = 0.0 ; }; #endif
函数的声明应该放在头文件中,内联函数的定义也应该放在头文件中。
【实例】分离式编译:
1 2 3 4 5 6 7 #ifndef CHAPTER6_H_INCLUDED #define CHAPTER6_H_INCLUDED int fact (int ) ;#endif
1 2 3 4 5 6 7 8 #include "Chapter6.h" using namespace std;int fact (int val) { if (val==0 || val==1 ) return 1 ; else return val * fact (val-1 ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> #include "Chapter6.h" using namespace std;int main () { int num; cout << "input an int num: " ; cin >> num; cout << num << "! = " << fact (num) <<endl; system ("pause" ); return 0 ; }
如上图,在终端中输入以下命令:
1 g++ factMain.cpp fact.cpp -o scexamp
标准库类型string
1 2 3 4 5 6 string s1,s2; cin>>s1>>s2; cout<<s1<<s2<<endl;
getline得到的string对象不包含换行符:
1 2 3 4 5 6 7 8 int main () { string line; while (getline (cin,line)){ cout<<line<<endl; } return 0 ; }
字面值和string对象相加:
cctype中的一些函数:
1 2 for (string::size_type i=0 ; i!=s.size (); i=i+2 ) s[i]='x' ;
从逻辑上讲,size()成员函数应该似乎返回整型数值,但事实上,size操作返回是string::size_type类型的值。string类类型和其他许多库类型都定义了一些配套类型(companion type)。通过这些配套类型,库函数的使用就与机器无关(machine-independent)。size_type就是这些配套类型中的一种。它定义为与unsigned型(unsigned int或unsigned long)具有相同含义,而且保证足够大的能够存储任意的string对象的长度。string::size_type在不同的机器上长度可以不同,并非固定。但只要使用该类型,就使得程序适合机器。string对象的索引也应为size_type类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main () { string s ("Hello World!!!" ) ; decltype (s.size ())punct_cnt = 0 ; for (auto c : s){ if (ispunct (c))++punct_cnt; } string orig = s; for (auto &c : s){ c = toupper (c); } cout<<s<<endl; }
标准库类型vector
访问vector的一种方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <vector> #include <iostream> int main () { std::vector<int > v{1 ,2 ,3 ,4 ,5 ,6 }; for (auto &i : v){ i*=i; } for (auto i : v){ std::cout<<i<<" " ; } std::cout<<std::endl; return 0 ; }
迭代器(iterator) 有迭代器的类型都拥有begin和end成员。如果容器为空,则begin和end返回的是同一个迭代器,都是尾后迭代器。
1 2 auto b = v.begin (), e = v.end ();
迭代器类型,iterator和const_iterator。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vector<int >::iterator it; string::iterator it2; vector<int >::const_iterator it3; string::const_iterator it4; vector<int > v; const vector<int > cv;auto it1 = v.begin (); auto it2 = cv.begin (); auto it3 = v.cbegin ();
结合解引用的成员访问:
1 2 3 4 5 6 vector<string> v; auto it = v.begin ();(*it).empty (); *it.empty (); it->empty ();
任何一种可能改变vector对象容量的操作,都会使得相应的迭代器失效。
迭代器运算:
数组 一种写法:int a[]={1,2,3};,[]内可以不填数字。
字符数组的特殊性:字符串字面值的结尾处有一个空字符。
1 2 3 4 char a1[] = {'C' ,'+' ,'+' }; char a2[] = {'C' ,'+' ,'+' ,'\0' }; char a3[] = "C++" ; const char a4[6 ] = "Danial" ;
复杂的数组声明:
1 2 3 4 5 int *ptrs[10 ]; int &refs[10 ] = ; int (*Parray)[10 ] = &arr; int (&arrRef)[10 ] = arr; int *(&arry)[10 ] = ptrs;
数组的begin和end函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <iostream> #include <iterator> int main () { int a[]={1 ,2 ,3 ,4 }; int *p1 = std::begin (a); int *p2 = std::end (a); for (;p1 != p2; p1++){ std::cout<<*p1<<" " ; } return 0 ; }
指针运算:
下标和指针:
1 2 3 4 int ia[]={0 ,2 ,4 ,6 ,8 };int *p = &ia[2 ]; int j = p[1 ]; int k = p[-2 ];
C风格字符串:处理函数定义在cstring头文件中。
1 2 char ca[]={'C' ,'+' ,'+' };cout<<strlen (ca)<<endl;
与旧代码的接口:
1 2 3 string s ("Hello World" ) ;char *str = s; const char *str = s.c_str ();
使用数组初始化vector对象:
1 2 3 4 int int_arr[]={0 ,1 ,2 ,3 ,4 ,5 };vector<int > ivec (std::begin(int_arr),std::end(int_arr)) ;vector<int > subVec (int_arr+1 ,int_arr+4 ) ;
多维数组 将所有元素初始化为0:int arr[10][20][30]={0};
初始化:
1 2 3 4 5 6 7 8 int ia2[3 ][4 ] = { {0 ,1 ,2 ,3 }, {4 ,5 ,6 ,7 }, {8 ,9 ,10 ,11 } }; int ia3[3 ][4 ] = {0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 }; int ia4[3 ][4 ] = {{0 },{4 },{8 }}; int ia5[3 ][4 ] = {0 ,3 ,6 ,9 };
下标引用:
使用范围for语句处理多维数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 size_t cnt = 0 ;for (auto &row : ia) for (auto &col : row) { col = cnt; ++cnt; } for (const auto &row : ia) for (auto col : row) cout << col << endl;
指针和多维数组:
1 2 3 4 5 6 7 for (auto p=ia; p!=ia+3 ; p++){ for (auto q=*p; q!=*p+4 ; q++) cout << *q << ' ' ; cout << endl; }
类型别名简化多维数组的指针:
1 2 3 4 5 6 7 using int_array = int [4 ];for (int_array *p = ia; p!=ia+3 ; p++){ for (int *q = *p; q!=*p+4 ; q++) cout << *q << ' ' ; cout << endl; }
表达式基础 概念:左值和右值,上网查。
如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。例如,对于int *p:
因为解引用运算符生成左值,所以decltype(*p)的结果是int&
因为取地址运算符生成右值,所以decltype(&p)的结果是int **
如果改变了某个运算对象的值,在表达式的其他地方不要再使用这个运算对象。
算术运算符 m%(-n)等于m%n,(-m)%n等于-(m%n)。
成员访问、条件、位运算符 成员访问运算符,ptr->mem等价于(*ptr).mem。
1 2 3 4 string s1 = "a string" , *p = &s1; auto n = s1.size ();n = (*p).size (); n = p->size ();
位运算符作用于整数类型。关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型 。
1 2 3 unsigned char bits = 0233 ; bits << 8 ;
使用位运算符:假设班级中有30个学生,用一个二进制位表示某个学生在测试中是否通过。例子如下:
1 2 3 4 unsigned long quizl = 0 ;quizl |= 1UL << 27 ; quizl &= ~(1UL << 27 ); bool status = quizl & (1UL << 27 );
sizeof和逗号运算符 1 2 3 4 5 6 7 8 9 10 11 12 Sales_data data, *p; sizeof (Sales_data); sizeof data; sizeof *p; sizeof p; sizeof data.revenue; sizeof Sales_data::revenue; constexpr size_t sz = sizeof (ia) / sizeof (*ia);int arr2[sz];
类型转换 看看就好,一般不用:
指针的转换:
0或字面值nullptr能够转换成任意指针类型
指向任意非常量的指针能够转换成void*
指向任意对象的指针能够转换成const void*
显式转换:强制转换cast-name<type>(expression),cast-name是static_cast、dynamic_cast、const_cast和reinterpret_cast中的一种。
1 2 3 4 5 6 int i,j;double slope = static_cast <double >(j) / i;double d;void *p = &d;double *dp = static_cast <double *>(p);
const_cast只能改变运算对象的底层const,对于将常量对象转换成非常量对象的行为,称为『去掉const性质(cast away the const)』。
const_cast可以移除底层const,或是给普通的类型添加底层const。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int main () { int a = 5 ; const int *cp = &a; int *p = const_cast <int *>(cp); *p = 4 ; cout << a; return 0 ; } int main () { const int a = 5 ; const int *cp = &a; int *p = const_cast <int *>(cp); *p = 4 ; cout << a; return 0 ; }
static_cast不能去掉const性质。const_cast不能改变类型。
reinterpret_cast通常为运算对象的位模式提供较低层次上的重新解释。
条件语句 switch-case,case标签必须是整型(小整型、bool型、short、char也都可以)常量表达式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int main () { switch (2 ){ case 1 : int c; break ; case 2 :{ cout<<"before c = " <<c<<endl; c = 1 ; int a = 1 ; cout<<"after c = " <<c<<endl; break ; } default : break ; } }
迭代语句 范围for语句:
1 2 3 4 5 6 7 8 9 10 vector<int > v = {0 ,1 ,2 ,3 ,4 ,5 ,6 }; for (auto &r : v){ r *= 2 ; } for (auto beg=v.begin (),end=v.end (); beg!=end; ++beg){ auto &r = *beg; r *= 2 ; }
跳转语句 goto语句:无条件跳转到同一函数内的另一条语句。一般情况下不要使用goto。
1 2 3 4 5 6 7 8 9 10 11 12 13 goto end; int ix = 10 ; end: ix = 42 ; begin: int sz = get_size (); if (sz<=0 ){ goto begin; }
异常处理 运行时的反常行为,例如读取或写入数据时失去数据库链接。
throw表达式:异常检测部分使用throw表达式来表示它遇到了无法解决的问题。
runtime_error是标准库异常类型的一种,定义在stdexcept头文件。它抛出一个异常,终止当前的函数,并把控制权交给处理异常的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Sales_item item1,item2; cin >> item1 >> item2; if (item1.isbn () == item2.isbn ()){ cout << item1+item2 << endl; return 0 ; }else { cerr << "Data must refer to same ISBN" << endl; return -1 ; } if (item1.isbn () != item2.isbn ()){ throw runtime_error ("Data must refer to same ISBN" ); } cout << item1+item2 <<endl;
try语句块:异常处理部分使用try语句块处理异常,可以有一个或多个catch。
1 2 3 4 5 6 7 8 9 10 11 12 13 while (cin>>item1>>item2){ try { }catch (runtime_error err){ cout << err.what () << "\nTry Again? Enter y or n" << endl; char c; cin >> c; if (!cin || c=='n' ) break ; } }
一套异常类:throw表达式和相关的catch子句之间传递异常的具体信息。这些异常分别定义在4个头文件中:
exception头文件:最通用的异常类exception,只报告异常的发生,不提供额外信息
stdexcept头文件:定义了几种常用的异常类
new头文件:bad_alloc异常类
type_info头文件:bad_cast异常类
函数基础 局部对象:
自动对象:生命周期从变量声明开始,到函数块末尾结束
局部静态对象:生命周期从变量声明开始,直到程序结束才销毁
1 2 3 4 5 size_t count_calls () { int a; static size_t ctr = 0 ; return ++ctr; }
参数传递 指针型变量在函数体中需要被改变的写法:
一维数组作为参数,除了将数组名传入函数外,为了规范化,还要将数组的大小作为参数传入:
1 2 3 int sum_arr (int att[] , int size) { }
数组名是首元素的地址,因此还可以写成:
1 2 3 int sum_arr (int *att , int size) { }
不管是哪种定义,使用函数时都是将数组名作为参数,比如:sum_arr (Ss , 66);。在函数内部对传入的数组进行修改,该数组本身的值也会改变。
若要防止在函数中修改数组,可以使用const:
1 int sum_arr (const int att[] , int size)
前面将数组的首元素的地址和长度传入,这样就可以处理所有元素。C++中引入了新的方式,即数组区间:传入数组的首元素地址和末尾地址,参数就是【数组名,数组名+长度】,这样也可以处理所有元素。进一步,也可以传入任意区间。例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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; } int A[Size] = {0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 } ;int sum = sum_arr (A,A+8 );
二维数组作为参数,与一维类似,有两种声明方式,但有所区别:
1 2 3 int sum (int A[ ][4 ] , int size) int sum (int (*A)[4 ] , int size)
至于使用方法都是一样,sum(A,4);。第二维长度有较严格的要求:
1 2 3 4 5 6 7 8 void f (int x[][5 ] , int mysize) { } int a[10 ][5 ];int b[10 ][3 ];f (a,10 ); f (b,10 );
如果函数无需改变引用形参的值,最好将其声明为常量引用:
1 2 3 bool isShorter (const string &s1, const string &s2) { return s1.size ()<s2.size (); }
C++允许将变量定义为数组的引用:
1 2 3 4 5 6 void print (int (&arr)[10 ]) { for (auto elem : arr){ cout<<elem<<endl; } }
main处理命令行选项。有时需要给main传实参,一种常见的情况是用户设置一组选项来确定函数所要执行的操作。例如,假定main函数位于可执行文件prog内,可以向程序传递下面的选项:
这些命令行选项通过两个(可选的)形参传递给main函数:
1 2 3 4 5 6 int main (int argc, char *argv[]) {}int main (int argc, char **argv) {}
以上面提供的命令行为例,argc应该等于5,argv应该包含如下的C风格字符串:
1 2 3 4 5 6 argv[0 ] = "prog" ; argv[1 ] = "-d" ; argv[2 ] = "-o" ; argv[3 ] = "ofile" ; argv[4 ] = "data0" ; argv[5 ] = 0 ;
含有可变形参的函数:参数个数不固定。如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型。
1 2 3 4 5 6 7 8 9 10 11 12 void error_msg (initializer_list<string> il) { for (auto beg=il.begin (); beg!=il.end (); ++beg){ cout<<*beg<<" " ; } cout<<endl; } if (excepted!=actual) error_msg ({"functionX" ,excepted,actual}); else error_msg ({"functionX" ,"okey" });
省略符形参:上网查。
返回类型和return语句 不要 返回局部对象的引用或指针:
1 2 3 4 5 6 7 8 9 const string &manip () { string ret; if (!ret.empty ()) return ret; else return "Empty" ; }
列表初始化返回值(C++11):
1 2 3 4 5 6 7 8 9 10 vector<string> process () { if (expected.empty ()) return {}; else if (expected==actual) return {"functionX" ,"okay" }; else return {"functionX" ,expected,actual}; }
main的返回值:
1 2 3 4 5 6 7 8 int main () { if (some_failure) return EXIT_FAILURE; else return EXIT_SUCCESS; }
函数重载 函数重载:函数名称相同但形参列表不同。
1 2 3 4 5 6 7 8 Record lookup (const Account&) ;bool lookup (const Account&) ; Record lookup (const Phone&) ;Record lookup (const Name&) ;Account acct; Phone phone; Record r1 = lookup (acct); Record r2 = lookup (phone);
const_cast和重载:
1 2 3 4 5 6 7 8 9 const string &shorterString (const string &s1, const string &s2) { return s1.size ()<=s2.size () ? s1 : s2; } string &shorterString (string &s1, string &s2) { auto &r = shorterString (const_cast <const string&>(s1), const_cast <const string&>(s2)); return const_cast <string&>(r); }
【实例】 函数重载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <iostream> void f () { std::cout << "该函数无须参数" << std::endl; } void f (int ) { std::cout<< "该函数有一个整型参数" << std::endl; } void f (int , int ) { std::cout<< "该函数有两个整型参数" << std::endl; } void f (double a, double b=3.14 ) { std::cout << "该函数有两个双精度浮点型参数" << std::endl; } int main () { f (2.56 , 42.0 ); f (42 ); f (42 , 0 ); f (2.56 , 3.14 ); return 0 ; }
输出结果:
1 2 3 4 5 6 7 Active code page: 65001 PS C:\Users\arrogance> cd "d:\c++code\" PS D:\c++code> if ($ ?) { g++ test.cpp -o test } ; if ($ ?) { .\test }该函数有两个双精度浮点型参数 该函数有一个整型参数 该函数有两个整型参数 该函数有两个双精度浮点型参数
特殊用途语言特性 默认实参:一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
1 2 3 4 5 6 7 8 9 10 11 typedef string::size_type sz;string screen (sz ht=24 , sz wid=80 , char backgrnd=' ' ) ;string mwindow; mwindow = screen (); mwindow = screen (66 ); mwindow = screen (66 ,256 ); mwindow = screen (66 ,256 ,'#' ); mwindow = screen (,,'?' ); mwindow = screen ('?' );
constexpr函数:能用于常量表达式的函数,函数的返回类型及所有的形参都是字面值类型。
函数体中必须有且仅有一条return语句
constexpr函数被隐式地指定为内联函数
constexpr函数并不要求返回常量表达式
1 2 3 4 5 6 7 constexpr int new_sz () {return 42 ;}constexpr int foo = new_sz (); constexpr size_t scale (size_t cnt) {return new_sz ()*cnt;}int arr[scale (2 )]; int i = 2 ;int a2[scale (i)];
调试帮助:只在开发过程中使用的代码,发布时屏蔽掉。
assert预处理宏,位于cassert头文件中。
1 2 3 assert (word.size ()>threshold);
NDEBUG预处理变量:assert的行为依赖NDEBUG预处理变量的状态,如果定义了NDEBUG,则assert无效。
1 2 #define NDEBUG #include <cassert>
除了用于assert外,也可以使用NDEBUG编写自己的调试代码:
1 2 3 4 5 6 7 void print (const int ia[], size_t size) {#ifndef NDEBUG cerr << __func__ << ": array size is " << size << endl; #endif }
函数匹配 这一节看看就好,感觉用处不太。
例子如下:
函数指针 函数指针,指针指向的是函数。
1 2 3 4 5 6 7 8 9 10 11 bool lengthCompare (const string&, const string&) ;bool (*pf)(const string&, const string&); pf = lengthCompare; pf = &lengthCompare; bool b1 = pf ("hello" ,"goodbye" );bool b2 = (*pf)("hello" ,"goodbye" );bool b3 = lengthCompare ("hello" ,"goodbye" );
在指向不同函数类型的指针间不存在转换规则(必须很精准的匹配才可以 )。
1 2 3 4 5 6 7 string::size_type sumLength (const string&, const string&) ;bool cstringCompare (const char *, const char *) ;pf = 0 ; pf = sumLength; pf = cstringCompare; pf = lengthCompare;
函数指针形参:
不能定义函数类型的形参,但形参可以是指向函数的指针。
1 2 3 4 5 6 7 8 9 void useBigger (const string &s1, const string &s2, bool pf(const string &, const string &)) ;void useBigger (const string &s1, const string &s2, bool (*pf)(const string &, const string &)) ;useBigger (s1,s2,lengthCompare);
通过使用类型别名,简化使用函数指针:
1 2 3 4 5 6 7 8 9 10 typedef bool Func (const string&, const string&) ;typedef decltype (lengthCompare) Func2 ; typedef bool (*FuncP) (const string&, const string&) ;typedef decltype (lengthCompare) *FuncP2 ; void useBigger (const string&, const string&, Func) ;void useBigger (const string&, const string&, FuncP2) ;
【练习6.54】 编写函数的声明,令其接受两个 int 形参并且返回类型也是 int ;然后声明一个 vector 对象,令其元素是指向该函数的指针。
1 2 3 4 int func (int , int ) ;vector<decltype (func)* > vF;
返回指向函数的指针:虽然不能返回一个函数,但是可以返回指向函数类型的指针。必须把返回类型写成指针形式,编译器不会自动处理。
1 2 3 4 5 6 using F = int (int *, int ); using PF = int (*)(int *, int ); PF f1 (int ) ; F f1 (int ) ; F *f1 (int ) ;
也可以用下面的形式直接声明f1:
1 int (*f1 (int )) (int *, int );
从里往外读:f1(int)是一个函数,这个函数返回的是一个指针(*f1(int)),这个指针指向的是一个函数 (int*, int),这个函数(int*, int)返回的是int型。
使用尾置返回类型的方式:
1 auto f1 (int ) -> int (*) (int *, int ) ;
使用尾置返回类型的其他例子:
1 2 3 4 string (&func ())[10 ];auto func () -> string (&) [10] ;
定义抽象数据类型 考虑如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct Sales_data { std::string isbn () const {return bookNo;} Sales_data& combine (const Sales_data&) ; double avg_price () const ; std::string bookNo; unsigned units_sold = 0 ; double revenue = 0.0 ; }; Sales_data add (const Sales_data&, const Sales_data&) ;std::ostream &print (std::ostream&, const Sales_data&) ;std::istream &read (std::istream&, Sales_data&) ;
成员都必须在类的内部声明 ,但成员函数体可以定义 在类内也可以在类外。
常量成员函数:类的成员函数后面加const,表明这个函数不会修改这个类对象的数据成员。
两种效果相同的写法:
1 2 3 std::string isbn () const {return bookNo;}std::string isbn () const {return this ->bookNo;}
关于this的详细解释参考《C++ primer》。
编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。因此成员体可以随意使用类中的其他成员而无需在意这些成员出现的次序。
1 2 3 4 5 6 7 double Sales_data::avg_price () const { if (units_sold) return revenue/units_sold; else return 0 ; }
定义一个返回this对象的函数:
1 2 3 4 5 6 Sales_data& Sales_data::combine (const Sales_data &rhs) { units_sold += rhs.units_sold; revenue += rhs.revenue; return *this ; }
定义类相关的非成员函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 istream& read (istream &is, Sales_data &item) { double price = 0 ; is >> item.bookNo >> item.units_sold >> price; item.revenue = price * item.uints_sold; return is; } ostream& print (ostream &os, const Sales_data &item) { os << item.isbn () << " " << item.units_sold << " " << item.revenue << " " << item.avg_price (); return os; } Sales_data add (const Sales_data &lhs, const Sales_data &rhs) { Sales_data sum = lhs; sum.combine (rhs); return sum; }
构造函数:
构造函数与类名同名,没有返回值,用来初始化类对象的数据成员。
类可以包括多个构造函数。
构造函数不能被声明为const的
当我们创建类的一个const对象时,直到构造函数完成初始化,对象才能真正得到“常量”属性
合成的默认构造函数(synthesized default constructor) :如果我们的类没有显式地定义构造函数,编译器会为我们隐式地定义一个默认构造函数。对于大多数类来说,这个『合成的默认构造函数』将按照如下规则初始化类的数据成员:
若存在类内的初始值,用它来初始化成员。
否则,默认初始化该成员。
只有当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct Sales_data { Sales_data () = default ; Sales_data (const std::string &s): bookNo (s) {} Sales_data (const std::string &s, unsigned n, double p): bookNo (s), units_sold (n), revenue (p*n) {} Sales_data (std::istream &); std::string isbn () const {return bookNo;} Sales_data& combine (const Sales_data&) ; double avg_price () const ; std::string bookNo; unsigned units_sold = 0 ; double revenue = 0.0 ; }; Sales_data::Sales_data (std::istream &is){ read (is, *this ); }
拷贝、赋值和析构:
管理动态内存的类通常不能依赖于编译器合成的版本。使用vector或string除外。
访问控制与封装 使用访问说明符加强类的封装性:
public:类的接口,在整个程序内可以被访问
private:封装(即隐藏)类的实现细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Sales_data {public : Sales_data () = default ; Sales_data (const std::string &s, unsigned n, double p): bookNo (s), units_sold (n), revenue (p*n) {} Sales_data (const std::string &s): bookNo (s) {} Sales_data (std::istream&); std::string isbn () const {return bookNo;} Sales_data& combine (const Sales_data&) ; private : double avg_price () const { return units_sold ? revenue/units_sold : 0 ; } std::string bookNo; unsigned units_sold = 0 ; double revenue = 0.0 ; };
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或函数成为它的友元 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Sales_data {friend Sales_data add (const Sales_data&, const Sales_data&) ;friend std::istream& read (std::istream&, Sales_data&) ;friend std::ostream& print (std::ostream&, const Sales_data&) ;public : Sales_data () = default ; Sales_data (const std::string &s, unsigned n, double p): bookNo (s), units_sold (n), revenue (p*n) {} Sales_data (const std::string &s): bookNo (s) {} Sales_data (std::istream&); std::string isbn () const {return bookNo;} Sales_data& combine (const Sales_data&) ; private : double avg_price () const { return units_sold ? revenue/units_sold : 0 ; } std::string bookNo; unsigned units_sold = 0 ; double revenue = 0.0 ; }; Sales_data add (const Sales_data&, const Sales_data&) ;std::istream& read (std::istream&, Sales_data&) ;std::ostream& print (std::ostream&, const Sales_data&) ;
友元声明只能出现在类定义的内部,但具体位置不限。友元不是类的成员,不受访问控制级别的约束。友元的声明仅指定访问的权限,不是通常意义上的函数声明。因此若希望类的用户能调用某个友元函数,最好在友元声明之外再对函数进行一次声明(有些编译器必须声明,有些可以省略。出于移植性的考虑,最好声明一下)。
类的其他特性 定义一个类型成员:
1 2 3 4 5 6 7 8 9 10 class Screen {public : typedef std::string::size_type pos; private : pos cursor = 0 ; pos height = 0 , width = 0 ; std::string contents; };
上面这样做的原因是,Screen的用户不需要知道Screen使用了一个string对象来存放它的数据,pos隐藏了细节。
成员函数也支持重载:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Screen {public : typedef std::string::size_type pos; Screen () = default ; Screen (pos ht, pos wd, char c): height (ht),width (wd),contents (ht*wd,c){} char get () const {return contents[cursor];} inline char get (pos ht, pos wd) const ; Screen& move (pos r, pos c) ; private : pos cursor = 0 ; pos height = 0 , width = 0 ; std::string contents; }; inline Screen& Screen::move (pos r, pos c) { pos row = r*width; cursor = row+c; return *this ; } char Screen::get (pos r, pos c) const { pos row = r*width; return contents[row+c]; }
可变数据成员(mutable data member):
1 2 3 4 5 6 7 8 9 10 11 class Screen {public : void some_member () const ; private : mutable size_t access_ctr; }; void Screen::some_member () const { ++access_ctr; }
类数据成员的初始值:
1 2 3 4 5 class Window_mgr {private : std::vector<Screen> screens{Screen (24 , 80 , ' ' )}; };
返回*this的成员函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Screen {public : Screen& set (char ) ; Screen& set (pos, pos, char ) ; }; inline Screen& Screen::set (char c) { contents[cursor] = c; return *this ; } inline Screen& Screen::set (pos r, pos col, char ch) { contents[r*width+col] = ch; return *this ; } myScreen.move (4 ,0 ).set ('#' );
从const成员函数返回*this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Screen {public : Screen& display (std::ostream &os) { do_display (os); return *this ; } const Screen& display (std::ostream &os) const { do_display (os); return *this ; } private : void do_display (std::ostream &os) const { os << contents; } };
类的声明:我们可以仅声明类而暂时不定义它。
这种声明也叫前向声明(forward declaration) ,对于类型Screen来说,它在声明之后定义之前是一个不完全类型(incomplete type) ,不完全类型只能在有限的情况下使用:
可以定义指向这种类型的指针或引用
可以声明(但不能定义)以不完全类型作为参数或返回类型的函数
1 2 3 4 class Link_screen { Link_screen *next; Link_screen *prev; };
类之间的友元关系:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Screen { friend class Window_mgr ; }; class Window_mgr {public : using ScreenIndex = std::vector<Screen>::size_type; void clear (ScreenIndex) ; private : std::vector<Screen> screens{Screen (24 , 80 , ' ' )}; }; void Window_mgr::clear (ScreenIndex i) { Screen &s = screens[i]; s.contents = string (s.height*s.width, ' ' ); }
友元关系不存在传递性。
令成员函数作为友元:
1 2 3 4 5 6 7 8 9 class Screen { friend void Window_mgr::clear (ScreenIndex) ; };
友元声明和作用域:参考《C++ Primer》
类的作用域 1 2 3 4 5 Screen::pos ht=24 , wd=80 ; Screen scr (ht, wd, ' ' ) ; Screen *p = &scr; char c = scr.get (); c = p->get ();
对比以下代码:
1 2 3 4 5 6 7 8 9 void Window_mgr::clear(ScreenIndex i){ Screen &s = screens[i]; s.contents = string (s.height*s.width, ' ' ); } Window_mgr::ScreenIndex Window_mgr::addScreen (const Screen &s) { screens.push_back (s); return screens.size ()-1 ; }
成员定义中的名字查找。以下代码仅作原理展示,不能作为作为写代码的满分参考(:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int height;class Screen {public : typedef std::string::size_type pos; void dummy_fcn (pos height) { cursor = width*height; } private : pos cursor = 0 ; pos height = 0 , width = 0 ; }; void Screen::dummy_fcn (pos height) { cursor = width * this ->height; cursor = width * Screen::height; cursor = width * ::height; }
构造函数再探 有时候初始化列表必不可少。如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始列表提供初始值:
1 2 3 4 5 6 7 8 9 10 11 12 13 class ConstRef {public : ConstRef (int ii); private : int i; const int ci; int &ri; }; ConstRef::ConstRef (int ii){ i = ii; ci = ii; ri = i; }
正确做法,显式地初始化引用和const成员:
1 ConstRef::ConstRef (int ii):i (ii), ci (ii), ri (i){}
成员初始化的顺序,构造函数初始值列表中的顺序不会影响实际的初始化顺序:
1 2 3 4 5 6 7 8 9 class X { int i; int j; public : X (int val):j (val), i (j){} };
默认实参和构造函数:
1 2 3 4 5 6 class Sales_data {public : Sales_data (std::string s = "" ):bookNo (s){} };
【练习7.38】有些情况下我们希望提供 cin 作为接受 istream& 参数的构造函数的默认实参,请声明这样的构造函数。
1 Sales_data (std::istream& is = std::cin){is >> *this ;}
此时该函数具有了默认构造函数的作用,因此我们原来声明的默认构造函数Sales_data()=default;应该去掉,否则会引起调用的二义性。
委托构造函数(把自己的一些或全部职责给了其他构造函数):
1 2 3 4 5 6 7 8 9 10 11 class Sales_data {public : Sales_data (std::string s, unsigned cnt, double price): bookNo (s), units_sold (cnt), revenue (cnt*price){} Sales_data ():Sales_data ("" ,0 ,0 ){} Sales_data (std::string s):Sales_data (s,0 ,0 ){} Sales_data (std::istream &is):Sales_data (){read (is,*this );} };
当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。在Sales_data类中,受委托的构造函数体恰好是空的。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。具体参阅配套习题第174页(练习7.41):
默认构造函数的作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 class NoDefault {public : NoDefault (const std::string&); }; struct A { NoDefault my_mem; }; A a; struct B { B (){} NoDefault b_member; };
在实际中,如果定义了其他构造函数,最好也提供一个默认构造函数。
隐式的类类型转换:
1 2 3 4 string null_book = "9-999-99999-9" ; item.combine (null_book);
只允许一步类类型转换:
1 2 3 4 item.combine ("9-999-99999-9" );
下面这三种写法是允许的:
1 2 3 item.combine (string ("9-999-99999-9" )); item.combine (Sales_data ("9-999-99999-9" )); item.combine (cin);
抑制构造函数定义的隐式转换:explicit(清楚、明白的)
1 2 3 4 5 6 7 8 9 10 11 class Sales_data {public : Sales_data ()=default ; Sales_data (const std::string &s, unsigned n, double p): bookNo (s), units_sold (n), revenue (p*n){} explicit Sales_data (const std::string &s) :bookNo(s){ } explicit Sales_data (std::istream&) ; }; item.combine (null_book); item.combine (cin);
explicit关键字只允许出现在类内的构造函数声明处:
1 2 3 4 explicit Sales_data::Sales_data (istream& is) { read (is, *this ); }
explicit构造函数只能用于直接初始化:
1 2 3 Sales_data item1 (null_book) ; Sales_data item2 = null_book;
为转换显式地使用构造函数:
1 2 3 4 item.combine (Sales_data (null_book)); item.combine (static_cast <Sales_data>(cin));
聚合类 (aggregate class) :
所有成员都是public的
没有定义任何构造函数
没有类内初始值
没有基类,也没有virtual函数
1 2 3 4 5 struct Data { int ival; string s; }; Data val1 = {0 , "Anna" };
字面值常量类:(或称“字面值类”)
数据成员都必须是字面值类型
类必须至少含有一个constexpr构造函数
如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数
类必须使用析构函数的默认定义,该成员负责销毁类的对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Debug {public : constexpr Debug (bool b=true ) :hw(b), io(b), other(b){ } constexpr Debug (bool h, bool i, bool o) :hw(h),io(i),other(o){ } constexpr bool any () {return hw||io||other;} void set_io (bool b) {io=b;} void set_hw (bool b) {hw=b;} void set_other (bool b) {hw=b;} private : bool hw; bool io; bool other; };
constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型:
1 2 3 4 5 6 constexpr Debug io_sub (false , true , false ) ; if (io_sub.any ()) cerr << "print appropriate error messages" << endl; constexpr Debug prod (false ) ; if (prod.any ()) cerr << "print an error message" << endl;
类的静态成员 与类本身关联,而不需要与每个对象关联。
1 2 3 4 5 6 7 8 9 10 11 12 class Account {public : void calculate () {amount+=amount*interestRate;} static double rate () {return interestRate;} static void rate (double ) ; private : std::string owner; double amount; static double interestRate; static double initRate () ; };
静态成员存在于任何对象之外,所有对象共享:
1 2 3 4 5 6 double r;r = Account::rate (); Account ac1; Account *ac2 = &ac1; r = ac1.rate (); r = ac2->rate ();
可以在类的内部也可以在类的外部定义静态成员函数 。在外部定义时,不能重复static关键字,static关键字只出现在类内部的声明语句中:
1 2 3 void Account::rate (double newRate) { interestRate = newRate; }
静态数据成员:
想要确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放到同一个文件中。
静态成员的类内初始化。通常,类的静态成员不该在类的内部初始化(因为里面仅仅是一个声明)。以下是特殊情况:
1 2 3 4 5 6 7 8 9 10 11 class Account {public : static double rate () {return interestRate;} static void rate (double ) ; private : static constexpr int period = 30 ; double daily_tbl[period]; }; constexpr int Account::period;
静态成员能用于某些场景,而普通成员不能:
1 2 3 4 5 6 7 8 class Bar {public : private : static Bar mem1; Bar *mem2; Bar mem3; };
可以使用静态成员作为默认实参,因为它本身不是对象的一部分:
1 2 3 4 5 6 7 class Screen {public : Screen& clear (char = bkground) ; private : static const char bkground; };
Sales_data 综合(实例) 目前,我个人更倾向于char* p;的写法。
关于这之中可能的争议:
https://stackoverflow.com/questions/6990726/correct-way-of-declaring-pointer-variables-in-c-c
注意:以下程序尚未经过仔细的测试。 (进一步的测试请转到【练习8.7】)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #ifndef SALES_DATA_H #define SALES_DATA_H #include <string> #include <iostream> class Sales_data { friend Sales_data add (const Sales_data& lhs, const Sales_data& rhs) ; friend std::istream& read (std::istream& is, Sales_data& item) ; friend std::ostream& print (std::ostream& os, const Sales_data& item) ; public : Sales_data () = default ; Sales_data (const std::string& s):bookNo (s){} Sales_data (const std::string& s,unsigned n,double p):bookNo (s),units_sold (n),revenue (p*n){} Sales_data (std::istream& is); std::string isbn () const {return bookNo;} Sales_data& combine (const Sales_data& item) ; private : double avg_price () const {return units_sold ? revenue/units_sold : 0 ;} std::string bookNo; unsigned units_sold = 0 ; double revenue = 0.0 ; }; Sales_data add (const Sales_data& lhs, const Sales_data& rhs) ;std::istream& read (std::istream& is, Sales_data& item) ;std::ostream& print (std::ostream& os, const Sales_data& item) ;#endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include "Sales_data.h" Sales_data::Sales_data (std::istream& is){ read (is,*this ); } Sales_data& Sales_data::combine (const Sales_data& item) { units_sold += item.units_sold; revenue += item.revenue; return *this ; } Sales_data add (const Sales_data& lhs, const Sales_data& rhs) { Sales_data sum = lhs; sum.combine (rhs); return sum; } std::istream& read (std::istream& is, Sales_data& item) { double price = 0 ; is >> item.bookNo >> item.units_sold >> price; item.revenue = price*item.units_sold; return is; } std::ostream& print (std::ostream& os, const Sales_data& item) { os << item.isbn () << " " << item.units_sold << " " << item.revenue << " " << item.avg_price ()<<std::endl; return os; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "Sales_data.h" using namespace std;int main () { string name = "BOOKONE" ; Sales_data book1 (name,18 ,2.6 ) ; Sales_data book2 (cin) ; print (cout,book1); print (cout,book2); system ("pause" ); }
IO类
关于宽字符 :参见《C++ primer》278 页。
类型ifstream和istringstream都继承自istream。因此,可以像使用istream对象一样来使用ifstream和istringstream对象。例如,可以对ifstream或istringstream对象调用getline,也可以用>>从一个ifstream或istringstream对象中读取数据。类似的,类型ofstream和ostringstream都继承自ostream。
IO对象无拷贝或赋值:
1 2 3 4 ofstream out1, out2; out1 = out2; ofstream print (ofstream) ; out2 = print (out2);
读写一个 IO 对象会改变其状态,因此传递和返回的引用不能是const的。
查询流的状态:
IO 库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。这个类型应作为一个位集合来使用。IO 库定义了 4 个iostate类型的constexpr值,表示特定的位模式。这些值用来表示特定类型的 IO 条件,可以与位运算符一起使用来一次性检测或设置多个标志位。
1 2 3 4 5 goodbit = 0x0 eofbit = 0x1 failbit = 0x2 badbit = 0x4
badbit表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法再使用了。在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。如果到达文件结束位置,eofbit和failbit都会被置位。goodbit的值为 0 ,表示流未发生错误。如果badbit、failbit和eofbit任一个被置位,则检测流状态的条件会失败。
标准库还定义了一组函数来查询这些标志位的状态。操作good在所有错误位均未置位的情况下返回 true,而bad、fail和eof则在对应错误位被置位时返回 true。此外,在badbit被置位时,fail也会返回true 。这意味着,使用good或fail是确定流的总体状态的正确方法。实际上,我们将流当作条件使用的代码就等价于!fail()。而eof和bad操作只能表示特定的错误。
管理条件状态:
流对象的rdstate成员返回一个iostate值,对应流的当前状态。setstate操作将给定条件位置位,表示发生了对应错误。clear成员是一个重载的成员:它有一个不接受参数的版本,而另一个版本接受一个iostate类型的参数。
clear不接受参数的版本清除(复位)所有错误标志位。执行clear()后,调用good会返回 true 。我们可以这样使用这些成员:
1 2 3 4 auto old_state = cin.rdstate (); cin.clear (); process_input (cin); cin.setstate (old_state);
带参数的clear版本接受一个iostate值:
1 2 3 4 5 6 7 8 9 cin.clear (cin.rdstate () & ~cin.failbit & ~cin.badbit);
【练习8.1】编写函数,接受一个 istream& 参数,返回值类型也是 istream& 。此函数须从给定流中读取数据,直到遇到 eof 停止。它将读取的数据打印在标准输出上。完成这些操作后,在返回流之前,对流进行复位,使其处于有效状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> #include <stdexcept> using namespace std;istream& f (istream& in) { int v; while (in>>v, !in.eof ()){ if (in.bad ()) throw runtime_error ("io-stream error" ); if (in.fail ()){ cerr << "Data error, please try again: " << endl; in.clear (); in.ignore (100 , '\n' ); continue ; } cout << v << endl; } in.clear (); return in; } int main () { cout << "Please enter some integers and press Ctrl+Z to end: " << endl; f (cin); return 0 ; }
关于上例代码的一些解释(注意:整理自网络,不能严格保证其正确性) :
在函数f中,in.ignore(100, '\n')的作用是忽略输入流中的一些字符,直到遇到换行符为止,或者忽略了 100 个字符。这里的换行符'\n'是因为在输入整数时,用户可能会在输入后按下回车键,导致换行符被输入流中。通过忽略换行符,可以清除输入流中的垃圾数据,使得下一个整数输入操作能够得到正确的输入。忽略字符的操作是在输入流中移动指针,使得下一次读取操作可以从正确的位置开始。
需要注意的是,如果忽略了指定的最大数量 n 仍然没有遇到终止字符 c,则会设置输入流的failbit标志,表示输入流状态错误。
它的一个常用功能就是用来清除以回车结束的输入缓冲区的内容,消除上一次输入对下一次输入的影响。例如,cin.ignore(1024, '\n'),通常把第一个参数设置得足够大,这样是为了只有第二个参数 ‘\n’ 起作用。所以这一句就是把回车(包括回车)之前的所有字符从输入缓冲流中清除出去。
如果默认不给参数的话,默认参数为cin.ignore(1, EOF),即把EOF前的1个字符清掉,没有遇到EOF就清掉一个字符然后结束。
in.clear()成员函数用于清除输入流的错误标志,但是它并不会清除输入流中的垃圾数据。
管理输出缓冲:
刷新输出缓冲区:
1 2 3 cout << "hi!" << endl; cout << "hi!" << flush; cout << "hi!" << ends;
unitbuf操纵符:
1 2 cout << unitbuf; cout << nounitbuf;
警告:如果程序崩溃,输出缓冲区不会被刷新。
关联输入和输出流:
tie有两个重载的版本:一个版本不带参数,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。tie的第二个版本接受一个指向ostream的指针,将自己关联到此ostream。即,x.tie(&o)将流x关联到输出流o。
既可以将一个istream对象关联到另一个ostream,也可以将一个ostream关联到另一个ostream。
1 2 3 4 5 6 cin.tie (&cout); ostream* old_tie = cin.tie (nullptr ); cin.tie (&cerr); cin.tie (old_tie);
每个流同时最多关联到一个流,但多个流可以同时关联到同一个ostream。
文件输入输出
使用文件流对象:
1 2 ifstream in (ifile) ; ofstream out;
在新 C++ 标准中,文件名既可以是库类型 string 对象,也可以是 C 风格字符数组。旧版本的标准库只允许 C 风格字符数组。
用fstream代替iostream&:在要求使用基类型对象的地方,可以用继承类型的对象来替代。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ifstream input (argv[1 ]) ;ofstream output (argv[2 ]) ;Sales_data total; if (read (input, total)){ Sales_data trans; while (read (input, trans)){ if (total.isbn ()==trans.isbn ()) total.combine (trans); else { print (output, total) << endl; total = trans; } } print (output, total) << endl; } else cerr << "No data ?!" << endl;
上面的代码中,重要的是对read和print的调用。虽然两个函数定义时指定的形参分别是istream&和ostream&,但我们可以向它们传递fstream对象。
成员函数open和close:
1 2 3 4 5 6 7 8 ifstream in (ifile) ; ofstream out; out.open (ifile + ".copy" ); if (out){ } in.close (); in.open (ifile + "2" );
如果调用open失败,failbit会被置位。如果open成功,流的状态good()会为true。
自动构造和析构:
1 2 3 4 5 6 7 8 9 for (auto p = argv+1 ; p != argv+argc; p++){ ifstream input (*p) ; if (input){ process (input); } else cerr << "couldn't open: " + string (*p); }
当一个fstream对象离开其作用域时,与之关联的文件会自动关闭。 即:当一个fstream对象被销毁时,close会自动被调用。
【练习8.4】 编写函数,以读模式打开一个文件,将其内容读入到一个string的vector中,将每一行作为一个独立的元素存于vector中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream> #include <fstream> #include <string> #include <vector> using namespace std;int main () { ifstream in ("D:\\c++code\\exercise8-4\\data.txt" ) ; if (!in){ cerr << "couldn't open file: data.txt" << endl; return -1 ; } string line; vector<string> words; while (getline (in, line)){ words.push_back (line); } in.close (); auto it = words.begin (); while (it!=words.end ()){ cout << *it <<endl; ++it; } return 0 ; }
1 2 3 4 5 6 7 //data.txt tianshangdetaiyang yueliangyigeyang yiduoyejuhua zhengpiaoliang nidemoyang songniyiduoshan/san chahua
1 2 3 4 5 6 7 //输出结果 tianshangdetaiyang yueliangyigeyang yiduoyejuhua zhengpiaoliang nidemoyang songniyiduoshan/san chahua
【练习8.5】重写上面的程序,将每个单词作为一个独立的元素进行存储。
【解答】将while(getline(in, line))改为while(in >> line)即可。
文件模式:
以out模式打开文件会丢弃已有数据:
1 2 3 4 5 6 7 ofstream out ("file1" ) ; ofstream out2 ("file1" , ofstream::out) ; ofstream out3 ("file1" , ofstream::out | ofstream::trunc) ;ofstream app ("file2" , ofstream::app) ; ofstream app2 ("file2" , ofstream::out | ofstream::app) ;
每次调用open时都会确定文件模式:
1 2 3 4 5 ofstream out; out.open ("scratchpad" ); out.close (); out.open ("previous" , ofstream::app); out.close ();
【练习8.7】修改上一节的书店程序,将结果保存到一个文件中。将输出文件名作为第二个参数传递给 main 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <iostream> #include <fstream> #include "Sales_data.h" using namespace std;int main (int argc, char * argv[]) { if (argc != 3 ){ cerr << "Please give the input and output file names" << endl; return -1 ; } ifstream in (argv[1 ]) ; if (!in){ cerr << "Unable to open the input file" << endl; return -1 ; } ofstream out (argv[2 ]) ; if (!out){ cerr << "Unable to open the output file" << endl; return -1 ; } Sales_data total; if (read (in, total)){ Sales_data trans; while (read (in, trans)){ if (total.isbn ()==trans.isbn ()) total.combine (trans); else { print (out, total) << endl; total = trans; } } print (out, total) << endl; } else cerr << "No data" << endl; return 0 ; }
文件 8_7file_in.txt 内容如下:
1 2 3 4 5 6 war&peace 2 8.8 war&peace 1 9 taoteching 3 3.3 taoteching 6 9 Pride&Prejudice 2 89.64 hewhochangedchina 1926 0.817
创建 8_7file_out.txt 文件,初始为空。此外还有之前提到的文件 Sales_data.h 和 Sales_data_Implementation.cpp ,将它们放至同一个文件夹中。
打开 PowerShell ,更改路径(以博主的机器为例):
1 cd D:\c++code\exercise8-7
键入以下命令执行分离式编译:
1 g++ 8 _7main.cpp Sales_data_Implementation.cpp -o 8 _7prog
键入命令:
1 .\8 _7prog.exe 8 _7file_in.txt 8 _7file_out.txt
打开 8_7file_out.txt ,发现输出如下:
1 2 3 4 5 6 7 8 9 war&peace 3 26.6 8.86667 taoteching 9 63.9 7.1 Pride&Prejudice 2 179.28 89.64 hewhochangedchina 1926 1573.54 0.817
如果键入:
1 .\8 _7prog.exe 8 _7file_in.txt 8 _7file_out.txt hana.txt
则 powershell 会显示:
1 Please give the input and output file names
string 流
使用istringstream:
考虑这样的例子:有一个文件,列出了一些人名和他们的电话号码。某些人只有一个号码,而另一些则有多个。输入文件格式如下:
1 2 3 morgan 2015552368 8625550123 drew 9735550130 lee 6095550132 2015550175 8005550000
核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct PersonInfo { string name; vector<string> phones; }; string line, word; vector<PersonInfo> people; while (getline (cin, line)){ PersonInfo info; istringstream record (line) ; record >> info.name; while (record >> word) info.phones.push_back (word); people.push_back (info); }
【练习8.9】 使用 练习8.1 中编写的函数打印一个istringstream对象的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <iostream> #include <sstream> #include <string> #include <stdexcept> using namespace std;istream& f (istream& in) { string v; while (in>>v, !in.eof ()){ if (in.bad ()) throw runtime_error ("io-stream error" ); if (in.fail ()){ cerr << "Data error, please try again: " << endl; in.clear (); in.ignore (1000 , '\n' ); continue ; } cout << v << endl; } in.clear (); return in; } int main () { ostringstream msg; msg << "C++ Primer 5th Edition" << endl; istringstream in (msg.str()) ; f (in); return 0 ; }
输出结果:
重复使用字符串流时,每次都用调用clear:
使用ostringstream:
考虑情景:我们需要验证并改变电话号码的格式。对于无效的电话号码,需要打印错误信息。
核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ostringstream os; for (const auto &entry : people){ ostringstream formatted, badNums; for (const auto &nums : entry.phones){ if (!valid (nums)){ badNums << " " << nums; } else formatted << " " << format(nums); } if (badNums.str ().empty ()) os << entry.name << " " << formatted.str () << endl; else cerr << "input error: " << entry.name << " invalid number(s) " << badNums.str () << endl; } cout << os.str () << endl;
顺序容器概述
forward_list和array是新 C++ 标准增加的类型。
forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。
NOTE : 新标准库的容器比旧版本快得多。现代 C++ 程序应该使用标准库容器,而不是更原始的数据结构,如内置数组。
容器库概览 一种合法的写法:
1 vector<vector<string>> lines;
较旧的编译器可能需要这样写:
1 vector<vector<string> > lines;
虽然可以在容器中保存几乎任何类型,但某些容器操作对元素类型有自己的特殊要求。我们可以定义某类容器(即便它的类型不支持特定操作),但这种情况下,就只能使用那些无特殊要求的容器操作。
例如,顺序容器构造函数的一个版本接受容器大小参数,它使用了元素类型的默认构造函数。但某些类没有默认构造函数。
1 2 3 vector<noDefault> v1 (10 , init) ; vector<noDefault> v2 (10 ) ;
一个更直观的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <iostream> #include <vector> class A {public : A (std::string b){a = b;} std::string getStr () const {return a;} private : std::string a; }; int main () { A aObj ("hello" ) ; std::vector<A> objA (5 , aObj) ; auto it = objA.cbegin (); for (; it!=objA.cend (); ++it){ std::cout << (*it).getStr () << std::endl; } return 0 ; }
上面的代码中,若第 13 行改为std::string aObj("hello");也是可以的,这里存在隐式转换。
若第 14 行写成std::vector<A> objA(5);,就会报错。
forward_list迭代器不支持递减运算符--。
迭代器范围(iterator range): [begin, end)
【练习9.5】 题目描述没什么看头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <iostream> #include <vector> using namespace std;using ivecit = vector<int >::iterator;ivecit search_vec (ivecit beg, ivecit end, int val) { for (; beg!=end; beg++){ if (*beg==val) return beg; } return end; } int main () { vector<int > ilist = {1 , 2 , 3 , 4 , 5 , 6 , 7 }; cout << search_vec (ilist.begin (), ilist.end (), 3 )-ilist.begin () << endl; cout << search_vec (ilist.begin (), ilist.end (), 8 )-ilist.begin () << endl; return 0 ; }
输出结果:
通过类型别名,可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type。如果需要元素类型的一个引用,可以使用reference或const_reference。
为了使用这些类型,必须显式地使用其类名:
1 2 list<string>::iterator iter; vector<int >::difference_type count;
begin和end:
1 2 3 4 5 6 7 list<string> a = {"Milton" , "Shakespeare" , "Austen" }; auto it1 = a.begin ();auto it2 = a.rbegin (); auto it3 = a.cbegin ();auto it4 = a.crbegin (); auto it7 = a.begin (); auto it8 = a.cbegin ();
容器定义和初始化:
一个容器初始化为另一个容器的拷贝时,容器类型和元素类型必须相同。不过,当传迭代器参数来拷贝一个范围时(该方法不适用于array),无此要求。
1 2 3 4 5 6 7 8 9 list<string> authors = {"Milton" , "Shakespeare" , "Austen" }; vector<const char *> articles = {"a" , "an" , "the" }; list<string> list2 (authors) ; deque<string> authList (authors) ; vector<string> words (articles) ; forward_list<string> words (articles.begin(), articles.end()) ;deque<string> authList (authors.begin(), it) ;
与顺序容器大小相关的构造函数:
1 2 3 4 vector<int > ivec (10 , -1 ) ; list<string> slis (10 , "hi!" ) ; forward_list<int > ifli (10 ) ; deque<string> sdeq (10 ) ;
如果元素类型是内置类型或是具有默认构造函数的类类型,可以只为构造函数提供一个容器大小参数。
只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
标准库 array 具有固定大小:
1 2 3 4 5 6 array<int ,5> ia1; array<int ,5> ia2 = {0 ,1 ,2 ,3 ,4 }; array<int ,5> ia3 = {42 }; array<int ,5> digits = {0 ,1 ,2 ,3 ,4 }; array<int ,5> copy = digits;
array 允许赋值:
1 2 3 4 array<int ,10> a1 = {0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 }; array<int ,10> a2 = {0 }; a1 = a2; a2 = {0 };
使用assign(仅顺序容器):
1 2 3 4 5 list<string> names; vector<const char *> oldstyle; names = oldstyle; names.assign (oldstyle.cbegin (), oldstyle.cend ());
assign的第二个版本:
1 2 3 4 list<string> slist1 (1 ) ; slist1.assign (10 , "Hiya!" );
使用swap:
1 2 3 vector<string> svec1 (10 ) ;vector<string> svec2 (24 ) ;swap (svec1, svec2);
除 array 外,swap 不对任何元素进行拷贝、删除、插入操作,因此是常数时间开销。
元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作之后都不会失效。它们仍指向swap操作之前所指向的那些元素。但是,在swap之后,这些元素已经属于不同的容器了。例如,假定iter在swap之前指向svec1[3]的string,那么在swap之后它指向svec2[3]的元素。与其他容器不同,对一个string调用swap会导致迭代器、引用和指针失效。
与其他容器不同,swap两个array会真正交换它们的元素。因此,交换两个array所需的时间与array中元素的数目成正比。
因此,对于array,在swap操作之后,指针引用和迭代器所绑定的元素保持不变但元素值已经与另一个array中对应元素的值进行了交换。
在新标准库中,容器既提供成员函数版本的swap,也提供非成员版本的swap。而早期标准库版本只提供成员函数版本的swap。非成员版本的swap在泛型编程中是非常重要的。统一使用非成员版本的swap是一个好习惯。
关系运算符:
1 2 3 4 5 6 7 8 vector<int > v1 = {1 ,3 ,5 ,7 ,9 ,12 }; vector<int > v2 = {1 ,3 ,9 }; vector<int > v3 = {1 ,3 ,5 ,7 }; vector<int > v4 = {1 ,3 ,5 ,7 ,9 ,12 }; v1 < v2 v1 < v3 v1 == v4 v1 == v2
比较两个容器实际上是进行元素的逐对比较。这些运算符的工作方式与string的关系运算类似:
如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等。
如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。
只有当容器的元素类型也定义了相应的比较运算符时,才可以用关系运算符比较两个容器:
1 2 3 vector<Sales_data> storeA, storeB; if (storeA < storeB){}
顺序容器操作
由于 string 是一个字符容器,我们也可以用push_back在 string 末尾添加字符:
1 2 3 4 void pluralize (size_t cnt, string& word) { if (cnt>1 ) word.push_back ('s' ); }
容器元素是拷贝。
list、forward_list、deque容器支持将元素插到容器头部:
1 2 3 4 list<int > ilist; for (size_t ix = 0 ; ix != 4 ; ++ix) ilist.push_front (ix);
insert成员提供了更一般的功能:
1 2 slist.insert (iter, "Hello!" );
1 2 3 4 5 6 7 8 vector<string> svec; list<string> slist; slist.insert (slist.begin (), "Hello!" ); svec.insert (svec.begin (), "Hello!" );
插入范围元素:
1 2 3 4 5 6 7 8 9 10 11 svec.insert (svec.end (), 10 , "Anna" ); vector<string> v = {"quasi" , "simba" , "frollo" , "scar" }; slist.insert (slist.begin (), v.end ()-2 , v.end ()); slist.insert (slist.end (), {"these" , "words" , "will" , "go" , "at" , "the" , "end" }); slist.insert (slist.begin (), slist.begin (), slist.end ());
使用insert返回值:
1 2 3 4 5 6 list<string> lst; auto iter = lst.begin ();while (cin>>word){ iter = lst.insert (iter, word); }
使用emplace操作:
1 2 3 4 5 6 7 8 9 c.emplace_back ("101-1-1" , 24 , 15.99 ); c.push_back ("101-1-1" , 24 , 15.99 ); c.push_back (Sales_data ("101-1-1" , 24 , 15.99 )); c.emplace_back (); c.emplace (iter, "101-1-1" ); c.emplace_front ("101-1-1" , 24 , 15.99 );
【练习9.22】 一个有点奇怪的题目,闲得慌可以看看。
访问元素:
访问成员函数返回的是引用:
1 2 3 4 5 6 7 if (!c.empty ()){ c.front () = 42 ; auto &v = c.back (); v = 1024 ; auto v2 = c.back (); v2 = 0 ; }
在上面的代码中,使用 auto 变量保存这些函数的返回值,如果希望使用此变量改变元素的值,应定义为引用类型。
从容器内部删除元素:
1 2 elem1 = slist.erase (elem1, elem2);
特殊的forward_list操作:
关于这些操作的实现细节,请参阅《C++ Primer》第313页。
1 2 3 4 5 6 7 8 9 10 11 forward_list<int > flst = {0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 }; auto prev = flst.before_begin ();auto curr = flst.begin ();while (curr != flst.end ()){ if (*curr%2 ) curr = flst.erase_after (prev); else { prev = curr; curr++; } }
【练习9.28】编写函数,接受一个forward_list<string>和两个string共三个参数。函数应在链表中查找第一个string,并将第二个string插入到紧接着第一个string之后的位置。若第一个string未在链表中,则将第二个string插入到链表末尾。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> #include <forward_list> using namespace std;void test_and_insert (forward_list<string>& sflst, const string& s1, const string& s2) { auto prev = sflst.before_begin (); auto curr = sflst.begin (); bool inserted = false ; while (curr!=sflst.end ()){ if (*curr==s1){ curr = sflst.insert_after (curr, s2); inserted = true ; } prev = curr; curr++; } if (!inserted) sflst.insert_after (prev, s2); } int main () { forward_list<string> sflst = {"Hello" , "!" , "world" , "!" }; test_and_insert (sflst, "Hello" , "Sucrose" ); for (auto curr=sflst.cbegin (); curr!=sflst.cend (); curr++){ cout << *curr << " " ; } cout << endl; return 0 ; }
改变容器大小:
1 2 3 4 list<int > ilist (10 , 42 ) ; ilist.resize (15 ); ilist.resize (25 , -1 ); ilist.resize (5 );
容器操作可能使迭代器失效。 这在不同情况下会有不同的表现,如果你是一个纠结于此类无聊问题的人,请翻阅《C++ primer(第五版)》第315页。
不要保存 end 返回的迭代器。 添加或删除元素的循环程序必须反复调用 end ,而不能在循环之前保存 end 返回的选代器,一直当作容器末尾使用。通常 C++ 标准库的实现中 end() 操作都很快,部分就是因为这个原因。
vector对象是如何增长的 vector 的底层其实仍然是定长数组,它能够实现动态扩容的原因是增加了避免数量溢出的操作。首先需要指明的是 vector 中元素的数量(长度)n 与它已分配内存最多能包含元素的数量(容量)N 是不一致的,vector 会分开存储这两个量。当向 vector 中添加元素时,如发现 n>N,那么容器会分配一个尺寸为 2N 的数组,然后将旧数据从原本的位置拷贝到新的数组中,再将原来的内存释放。尽管这个操作的渐进复杂度是 O(n),但是可以证明其均摊复杂度为 O(1),而在末尾删除元素和访问元素则都仍然是 O(1) 的开销。 因此,只要对 vector 的尺寸估计得当并善用resize()和reserve(),就能使得 vector 的效率与定长数组不会有太大差距。
reserve并不改变容器中元素的数量,它仅影响 vector 预先分配多大的内存空间。
只有当需要的内存空间超过当前容量时,reserve调用才会改变 vector 的容量。如果需求大小大于当前容量,reserve至少分配与需求一样大的内存空间(可能更大)。 如果需求大小小于或等于当前容量,reserve什么也不做。特别是,当需求大小小于当前容量时,容器不会退回内存空间。因此,在调用reserve之后,capacity将会大于或等于传递给reserve的参数。 这样,调用reserve永远也不会减少容器占用的内存空间。类似的,resize成员函数只改变容器中元素的数目,而不是容器的容量。我们同样不能使用resize来减少容器预留的内存空间。 在新标准库中,我们可以调用shrink_to_fit来要求deque、vector或string退回不需要的内存空间。此函数指出我们不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求。也就是说,调用shrink_to_fit也并不保证一定退回内存空间。
capacity和size:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 vector<int > ivec; cout << "ivec: size: " << ivec.size () << " capacity: " << ivec.capacity () << endl; for (vector<int >::size_type ix = 0 ; ix!=24 ; ix++) ivec.push_back (ix); cout << "ivec: size: " << ivec.size () << " capacity: " << ivec.capacity () << endl; ivec.reserve (50 ); cout << "ivec: size: " << ivec.size () << " capacity: " << ivec.capacity () << endl; while (ivec.size ()!=ivec.capacity ()) ivec.push_back (0 ); cout << "ivec: size: " << ivec.size () << " capacity: " << ivec.capacity () << endl; ivec.push_back (42 ); cout << "ivec: size: " << ivec.size () << " capacity: " << ivec.capacity () << endl; ivec.shrink_to_fit (); cout << "ivec: size: " << ivec.size () << " capacity: " << ivec.capacity () << endl;
只有在执行insert操作时size与capacity相等,或者调用resize或reserve时给定的大小超过当前capacity,vector 才可能重新分配内存空间。会分配多少超过给定容量的额外空间,取决于具体实现。
额外的string操作
substr 操作:
1 2 3 4 5 string s ("hello world" ) ;string s2 = s.substr (0 , 5 ); string s3 = s.substr (6 ); string s4 = s.substr (6 , 11 ); string s5 = s.substr (12 );
【练习9.41】编写程序,从一个vector<char>初始化一个string。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <string> #include <vector> #include <iostream> int main () { std::vector<char > chvec = {'h' ,'e' ,'l' ,'l' ,'o' }; std::string s1 (chvec.data(), chvec.size()) ; std::cout << s1 << std::endl; return 0 ; }
【练习9.42】假定你希望每次读取一个字符存入一个 string 中,而且知道最少需要读取 100 个字符,如何提高程序性能?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <iostream> #include <vector> #include <string> using namespace std;void input_string (string& s) { s.reserve (100 ); char c; while (cin>>c){ s.push_back (c); } } int main () { string s; input_string (s); cout << s << endl; return 0 ; }
除了接受迭代器的insert和erase版本外,string 还提供了接受下标的版本:
1 2 s.insert (s.size (), 5 , '!' ); s.erase (s.size ()-5 , 5 );
还提供了接受 C 风格字符数组的insert和assign版本:
1 2 3 const char * cp = "Stately, plump Buck" ;s.assign (cp, 7 ); s.insert (s.size (), cp+7 );
我们也可以指定来自其他 string 或子字符串的字符插入到当前 string 中:
1 2 3 4 string s = "some string" , s2 = "some other string" ; s.insert (0 , s2); s.insert (0 , s2, 0 , s2.size ());
append和replace函数:
1 2 3 4 5 6 7 8 9 10 11 12 string s ("C++ Primer" ) , s2 = s;s.insert (s.size (), " 4th Ed." ); s2.append (" 4th Ed." ); s.erase (11 , 3 ); s.insert (11 , "5th" ); s2.replace (11 , 3 , "5th" );
【练习9.43 & 练习9.44】如果你真的觉得题目描述有看头的话,就翻书看吧。
博主的代码,使用 KMP 算法实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include <iostream> #include <string> #include <vector> #include <stack> void getNext (std::vector<int >& mynext, std::string& t) { int j = 0 ; mynext.push_back (0 ); for (int i=1 ; i<t.size (); i++){ while (j>0 && t[i]!=t[j]) j=mynext[j-1 ]; if (t[i]==t[j]) j++; mynext.push_back (j); } } std::stack<int > kmpfind (std::string& s, std::string& t) { std::vector<int > mynext; std::stack<int > tmpans; getNext (mynext, t); int j = 0 ; for (int i = 0 ; i < s.size (); i++) { while (j > 0 && s[i] != t[j]) { j = mynext[j-1 ]; } if (s[i]==t[j]) j++; if (j==t.size ()) { tmpans.push (i-t.size ()+1 ); j = mynext[j-1 ]; } } return tmpans; } std::string convert2sth (std::string& s, std::string& oldVal, std::string& newVal) { std::stack<int > ayaka = kmpfind (s, oldVal); while (!ayaka.empty ()){ s.replace (ayaka.top (), oldVal.size (), newVal); ayaka.pop (); } return s; } int main () { std::string s, oldVal, newVal; std::cin >> s >> oldVal >> newVal; std::cout << convert2sth (s, oldVal, newVal); return 0 ; }
当然,我的这么一通操作到底是不是画蛇添足,不得而知。相关的讨论:C++string中find函数是用什么算法实现的?他的时间复杂度如何?实际比手写KMP效率相比如何?
书中给出的参考答案:
1 2 3 4 5 6 7 8 9 void replace_string (string& s, const string& oldVal, const string& newVal) { int p = 0 ; while ((p=s.find (oldVal, p))!=string::npos){ s.replace (p, oldVal.size (), newVal); p += newVal.size (); } }
【练习9.45 & 练习9.46】 题干没什么可看的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void name_string (string& name, const string& prefix, const string& suffix) { name.insert (name.begin (), 1 , ' ' ); name.insert (name.begin (), prefix.begin (), prefix.end ()); name.append (" " ); name.append (suffix.begin (), suffix.end ()); } void name_string (string& name, const string& prefix, const string& suffix) { name.insert (0 , " " ); name.insert (0 , prefix); name.insert (name.size (), " " ); name.insert (name.size (), suffix); }
string 搜索函数返回一个string::size_type值,该类型是一个unsigned类型。
1 2 3 4 5 6 7 8 9 10 string name ("AnnaBelle" ) ;auto pos1 = name.find ("Anna" ); string numbers ("0123456789" ) , name ("r2d2" ) ;auto pos = name.find_first_of (numbers);string dept ("03714p3" ) ;auto pos = dept.find_first_not_of (numbers);
查找一整个字符串的例子:
1 2 3 4 5 6 7 8 9 10 11 12 string numbers ("0123456789" ) , name ("r2d2" ) ;string::size_type pos = 0 ; while ((pos=name.find_first_of (numbers, pos))!=string::npos){ cout << "found number at index: " << pos << " element is " << name[pos] << endl; pos++; }
逆向搜索:
1 2 3 string river ("Mississippi" ) ;auto first_pos = river.find ("is" ); auto last_pos = river.rfind ("is" );
【练习9.49】 如果一个字母延伸到中线之上,如 d 或 f ,则称其有上出头部分(ascender)。如果一个字母延伸到中线之下,称其有下出头部分(descender)。编写程序,读入一个单词文件,输出最长的既不包括上出头部分,也不包括下出头部分的单词。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <iostream> #include <fstream> #include <string> using namespace std;void find_longest_word (ifstream& in) { string s, longest_word; int maxlen = 0 ; while (in >> s){ if (s.find_first_of ("bdfghjklpqty" )!=string::npos) continue ; cout << s << " " ; if (maxlen < s.size ()){ maxlen = s.size (); longest_word = s; } } cout << endl << "the longest string is: " << longest_word << endl; } int main (int argc, char * argv[]) { ifstream in (argv[1 ]) ; if (!in){ cerr << "cannot open file." << endl; return -1 ; } find_longest_word (in); return 0 ; }
输入文件:
1 2 3 4 5 6 asfdfva asdfe mm asdf/:o? asdf sfg aabb jjj kkk s d asdfasf werg aaa
命令及输出:
1 2 3 4 5 PS C:\Users\arrogance> cd D:\c++code\exercise9-49 PS D:\c++code\exercise9-49 > g++ 949 main.cpp -o 949 progPS D:\c++code\exercise9-49 > .\949 prog.exe 949 in.txtmm s aaa the longest string is: aaa
compare函数:
数值转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> #include <string> using namespace std;int main () { int i = 42 ; string s = to_string (i); cout << s <<endl; double d = stod (s); cout << d << endl; double akashi = 42.23 ; s = to_string (akashi); cout << stod (s) << endl << stoi (s) << endl; string s2 = "pi=3.14!!!??" ; d = stod (s2.substr (s2.find_first_of ("+-.0123456789" ))); cout << d << endl; return 0 ; }
【练习9.51】设计一个类,它有三个 unsigned 成员,分别表示月、日、年。为其编写构造函数,接受一个表示日期的 string 参数。你的构造函数应该能处理不同数据格式,如January 1,1990、1/1/1900、Jan 1 1900等。
我的代码,注意没有检查格式错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <iostream> #include <string> #include <map> class mdate { friend std::ostream& print (std::ostream& os, const mdate& item) ; public : mdate () = default ; mdate (const std::string& s){ std::string::size_type pos = 0 , pre = 0 ; std::string wilddata[3 ] = {"" ,"" ,"" }; int k = 0 ; while ((pos=s.find_first_of (" ,/" , pos))!=std::string::npos){ wilddata[k]=s.substr (pre, pos-pre); pos++; k++; pre = pos; } wilddata[2 ] = s.substr (pre); dd = stol (wilddata[1 ]); yy = stol (wilddata[2 ]); if (mm2num.count (wilddata[0 ])!=0 ){ mm = mm2num[wilddata[0 ]]; } else mm = stol (wilddata[0 ]); } private : unsigned long yy; unsigned long mm; unsigned long dd; static std::map<std::string,unsigned long > mm2num; }; std::map<std::string, unsigned long > mdate::mm2num = { {"January" , 1 }, {"Jan" , 1 }, {"February" , 2 }, {"Feb" , 2 }, {"March" , 3 }, {"Mar" , 3 }, {"April" , 4 }, {"Apr" , 4 }, {"May" , 5 }, {"May" , 5 }, {"June" , 6 }, {"Jun" , 6 }, {"July" , 7 }, {"Jul" , 7 }, {"August" , 8 }, {"Aug" , 8 }, {"September" , 9 }, {"Sept" , 9 }, {"October" , 10 }, {"Oct" , 10 }, {"November" , 11 }, {"Nov" , 11 }, {"December" , 12 }, {"Dec" , 12 } }; std::ostream& print (std::ostream& os, const mdate& item) { os << "中文表述习惯为:" << item.yy << "年" << item.mm << "月" << item.dd << "日" << std::endl; return os; } int main () { std::string line; while (getline (std::cin,line)){ if (line=="quit" )break ; mdate akashi (line) ; print (std::cout, akashi); } return 0 ; }
输入输出及命令行信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Oct 23 2003 中文表述习惯为:2003年10月23日 November 11 2018 中文表述习惯为:2018年11月11日 Jan 1,1998 中文表述习惯为:1998年1月1日 Aug,2/2015 中文表述习惯为:2015年8月2日 May/12 2098 中文表述习惯为:2098年5月12日 2 2 2001 中文表述习惯为:2001年2月2日 2/1/1023 中文表述习惯为:1023年2月1日 quit Press any key to continue . . .
以下给出检查某些错误的可能思路:
若某些特立独行的用户输入诸如/////之类的数据,或者连续多个空格等,会导致数组越界。为此需要重新编写 while 循环内的语句。
我们的思路是先将输入分成三块,然后分别在块内检查是否合法。如果依靠合法的分隔符都无法分为三块,则直接提示输入格式错误。
其他细节不表。
1 2 3 4 5 6 7 while ((pos=s.find_first_of (" ,/" , pos))!=std::string::npos){ if (pos!=pre) wilddata[k++]=s.substr (pre, pos-pre); pre = ++pos; } if (k!=2 ){}wilddata[2 ] = s.substr (pre);
习题册给出的案例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 #ifndef DATE_H_INCLUDED #define DATE_H_INCLUDED #include <iostream> #include <string> #include <stdexcept> using namespace std;class date {friend ostream& operator <<(ostream&, const date&);public : date () = default ; date (string& ds); unsigned y () const {return year;} unsigned m () const {return month;} unsigned d () const {return day;} private : unsigned year, month, day; }; const string month_name[] = {"January" , "February" , "March" , "April" , "May" , "June" , "July" , "August" , "September" , "October" , "November" , "December" }; const string month_abbr[] = {"Jan" , "Feb" , "Mar" , "Apr" , "May" , "Jun" , "Jul" , "Aug" , "Sept" , "Oct" , "Nov" , "Dec" }; const int days[] = {31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 };inline int get_month (string& ds, int & end_of_month) { int i,j; for (i=0 ; i<12 ; i++){ for (j=0 ; j<month_abbr[i].size (); j++){ if (ds[j]!=month_abbr[i][j]) break ; } if (j==month_abbr[i].size ())break ; } if (i==12 ) throw invalid_argument ("不是合法月份名" ); if (ds[j]==' ' ){ end_of_month = j+1 ; return i+1 ; } for (; j<month_name[i].size (); j++) if (ds[j]!=month_name[i][j]) break ; if (j==month_name[i].size () && ds[j]==' ' ){ end_of_month = j+1 ; return i+1 ; } throw invalid_argument ("不是合法月份名" ); } inline int get_day (string& ds, int month, int & p) { size_t q; int day = stoi (ds.substr (p), &q); if (day<1 || day>days[month]) throw invalid_argument ("不是合法日期值" ); p += q; return day; } inline int get_year (string& ds, int & p) { size_t q; int year = stoi (ds.substr (p), &q); if (p+q<ds.size ()) throw invalid_argument ("非法结尾内容" ); return year; } date::date (string& ds){ int p; size_t q; if ((p=ds.find_first_of ("0123456789" ))==string::npos) throw invalid_argument ("没有数字,非法日期" ); if (p>0 ){ month = get_month (ds, p); day = get_day (ds, month, p); if (ds[p]!=' ' && ds[p]!=',' ) throw invalid_argument ("非法间隔符" ); p++; year = get_year (ds, p); } else { month = stoi (ds, &q); p = q; if (month<1 || month>12 ) throw invalid_argument ("不是合法月份值" ); if (ds[p++]!='/' ) throw invalid_argument ("非法间隔符" ); day = get_day (ds, month, p); if (ds[p++]!='/' ) throw invalid_argument ("非法间隔符" ); year = get_year (ds, p); } } ostream& operator <<(ostream& out, const date& d){ out << d.y () << "年" << d.m () << "月" << d.d () << "日" << endl; return out; } #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <iostream> #include <string> #include "9_51head.h" using namespace std;int main () { string dates[] = {"Jan 1,2014" , "February 1 2014" , "3/1/2014" , "3 1 2014" }; try { for (auto ds : dates){ date d1 (ds) ; cout << d1; } } catch (invalid_argument e){ cout << e.what () << endl; } return 0 ; }
输出:
1 2 3 4 5 2014年1月1日 2014年2月1日 2014年3月1日 非法间隔符
注意这个案例代码的格式是严格按照题目要求的,稍有不符就判错了。
容器适配器 三个顺序容器适配器:stack、queue和priority_queue。
一个适配器是一种机制,能使得某事物的行为看起来像另一种事物一样。
例如,stack适配器接受一个顺序容器(array和forward_list除外),并使其操作起来像一个stack一样。
所有的适配器都要求容器具有添加、删除及方便访问尾元素的能力。
定义一个适配器。该部分内容较晦涩且实际用途不明(至少在我看来是绕了一个大圈实现了某种功能?),具体参阅第五版《C++ primer 中文版》第 329 页。
默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。我们可以创建适配器时,通过第二个参数来指定容器类型。
1 stack<int , vector<int >> intStack;
注意:上图中,q.pop()注释写错了。应该为:“删除首元素 … 不返回此元素”。
泛型算法概述 顺序容器只定义了很少的操作:在多数情况下,我们可以添加和删除元素、访问首尾元素、确定容器是否为空以及获得指向首元素或尾元素之后位置的迭代器。
我们可以想象用户可能还希望做其他很多有用的操作:查找特定元素、替换或删除一个特定值、重排元素顺序等。
标准库并未给每个容器都定义成员函数来实现这些操作,而是定义了一组泛型算法(generic algorithm) :称它们为“算法”,是因为它们实现了一些经典算法的公共接口,如排序和搜索;称它们是“泛型的”,是因为它们可以用于不同类型的元素和多种容器类型(不仅包括标准库类型,如 vector 或 list,还包括内置的数组类型),以及我们将看到的,还能用于其他类型的序列。
大多数算法都定义在头文件algorithm中。标准库还在头文件numeric中定义了一组数值泛型算法。
一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。
例如,我们有一个 int 的 vector :
1 2 3 4 int val = 42 ;auto result = find (vec.cbegin (), vec.cend (), val);cout << "The value" << val << (result == vec.cend () ? " is not present" : " is present" ) << endl;
例如,一个 string 的 list :
1 2 string val = "a value" ; auto result = find (lst.cbegin (), lst.cend (), val);
类似的,由于指针就像内置数组上的迭代器一样,我们可以用 find 在数组中查找值:
1 2 3 int ia[] = {27 , 210 , 12 , 47 , 109 , 83 };int val = 83 ;int * result = find (begin (ia), end (ia), val);
上例中使用了标准库的begin和end函数,来获得指向 ia 中首元素和尾元素之后位置的指针,并传递给 find .
还可以在序列的子范围中查找。例如,在ia[1]、ia[2]、ia[3]中查找给定元素:
1 auto result = find (ia+1 , ia+4 , val);
迭代器令算法不依赖于容器,但算法依赖于元素类型的操作。
未完待续
https://www.bilibili.com/video/BV1z64y1U7hs?p=52
OOP 概述 面向对象程序设计(object-oriented programming)的核心思想:
数据抽象:接口与实现分离
继承:定义相似的类,并对其相似关系建模
动态绑定:在一定程度上忽略相似类的区别,以统一的方式使用它们
通过继承(inheritance),联系在一起的类构成一种层次关系
基类(base class):定义共同拥有的成员
派生类(derived class):定义特有的成员
虚函数(virtual function):基类希望派生类各自定义自己合适的版本
1 2 3 4 5 6 7 8 9 10 11 12 13 class Quote {public : std::string isbn () const ; virtual double net_price (std::size_t n) const ; }; class Bulk_quote : public Quote{public : double net_price (std::size_t ) const override ; };
如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地 也是虚函数。
动态绑定(dynamic binding),我们能用同一段代码分别处理派生类和基类。
1 2 3 4 5 6 7 8 9 10 double print_total (ostream& os, const Quote& item, size_t n) { double ret = item.net_price (n); os << "ISBN: " << item.isbn () << "#sold: " << n << "total due: " << ret << endl; return ret; } print_total (cout, basic, 20 );print_total (cout, bulk, 20 );
使用基类的引用(或指针)调用一个虚函数时,将发生动态绑定(也叫运行时绑定:run-time binding)。
定义基类和派生类 protected访问运算符:基类希望它的派生类有权访问该成员,同时禁止其他用户访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Quote {public : Quote () = default ; Quote (const std::string& book, double sales_price): bookNo (book), price (sales_price){} std::string isbn () const {return bookNo;} virtual double net_price (std::size_t n) const { return n*price; } virtual ~Quote () = default ; private : std::string bookNo; protected : double price = 0.0 ; }; class Bulk_quote : public Quote{ public : Bulk_quote () = default ; Bulk_quote (const std::string&, double , std::size_t , double ); double net_price (std::size_t ) const override ; private : std::size_t min_qty = 0 ; double discout = 0.0 ; };
protected访问说明符的作用是控制派生类从基类继承来的成员是否对派生类的用户可见。
如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。同时,在需要基类的引用或指针的地方,都可以使用派生类的对象。
派生类必须使用基类的构造函数来初始化继承来的成员:
1 2 Bulk_quote (const std::string& book, double p, std::size_t qty, double disc): Quote (book, p), min_qty (qty), discount (disc){}
重写net_price():
1 2 3 4 5 6 double Bulk_quote::net_price (size_t cnt) const { if (cnt>=min_qty) return cnt*(1 -discount)*price; else return cnt*price; }
继承与静态成员:
如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Base {public : static void statmem () ; }; class Derived : public Base{ void f (const Derived&) ; }; void Derived::f (const Derived& derived_obj) { Base::statmem (); Derived::statmem (); derived_obj.statmem (); statmem (); }
派生类的声明:
1 2 class Bulk_quote : public Quote; class Bulk_quote ;
如果要将某个类用作基类,则该类必须已经定义。
一个类是基类,同时也可以是一个派生类:
1 2 3 class Base {};class D1 : public Base{};class D2 : public D1{};
防止继承发生:
1 2 3 4 5 class NoDerived final {}; class Base {};class Last final : Base{};class Bad : NoDerived{}; class Bad2 : Last{};
使用基类的引用(或指针)时,实际上编译器并不清楚所绑定对象的真实类型。
静态类型(static type):编译时已知
动态类型(dynamic type):运行时才可知
不存在从基类向派生类的隐式类型转换:
1 2 3 4 5 6 7 8 Quote base; Bulk_quote* bulkP = &base; Bulk_quote& bulkRef = base; Bulk_quote bulk; Quote* itemP = &bulk; Bulk_quote* bulkP = itemP;
在对象间不存在类型转换:
1 2 3 4 5 Bulk_quote bulk; Quote item (bulk) ; item = bulk;
【练习15.10】 回忆在 8.1 节中的讨论,解释第 284 页中将ifstream传递给Sales_data的read函数的程序是如何工作的。
【答】 在要求使用基类型对象的地方,可以使用派生类型的对象来代替,是静态类型和动态类型不同的典型例子。
虚函数 对虚函数的调用可能在运行时才被解析:
1 2 3 4 5 6 7 8 9 10 Quote base ("0-201-1" , 50 ) ;print_total (cout, base, 10 );Bulk_quote derived ("0-201-1" , 50 , 5 , .19 ) ;print_total (cout, derived, 10 );base = derived; base.net_price (20 );
基类中的虚函数在派生类中隐式地也是一个虚函数。该函数在基类中的形参必须与派生类中的形参严格匹配。
final和override说明符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct B { virtual void f1 (int ) const ; virtual void f2 () ; void f3 () ; }; struct D1 : B{ void f1 (int ) const override ; void f2 (int ) override ; void f3 () override ; void f4 () override ; }; struct D2 : B{ void f1 (int ) const final ; }; struct D3 : D2{ void f2 () ; void f1 (int ) const ; };
如果虚函数使用默认实参,基类和派生类中定义的默认实参最好一致。
回避虚函数的机制:
1 2 3 double undiscounted = baseP->Quote::net_price (42 );
什么时候我们需要回避虚函数的默认机制呢?通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时。在此情况下,基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本需要执行一些与派生类本身密切相关的操作。 如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。
【练习15.11】 为你的Quote类体系添加一个名为debug的虚函数,令其分别显示每个类的数据成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Quote {public : virtual void debug () { cout << "bookNo=" << bookNo << " price=" << price << endl; } private : protected : }; class Bulk_quote : public Quote{public : virtual void debug () { Quote::debug (); cout << "min_qty=" << min_qty << " discount=" << discount << endl; } private : };
抽象基类 需求:现在我们需要支持多种不同的折扣策略。共同点是每个折扣策略都需要一个购买量值和折扣值。
分析:我们可以定义一个Disc_quote类来支持不同的折扣策略,其中Disc_quote负责保存购买量值和折扣值。但是,Disc_quote类中的net_price()函数是没有实际含义的,为了防止用户编写出无意义的代码(具体查阅第五版《C++ primer(中文版)》第 540 页),我们必须重新考虑。我们根本就不希望用户创建Disc_quote对象,Disc_quote类表示的是一本打折书籍的通用概念,而非某种具体的折扣策略。
做法:将net_price()定义为纯虚(pure virtual) 函数。一个纯虚函数无须定义,在声明语句的分号之前书写=0就可以将一个虚函数说明为纯虚函数。
1 2 3 4 5 6 7 8 9 10 11 12 class Disc_quote : public Quote{public : Disc_quote () = default ; Disc_quote (const std::string& book, double price, std::size_t qty, double disc): Quote (book, price), quantity (qty), discount (disc){} double net_price (std::size_t ) const = 0 ; protected : std::size_t quantity = 0 ; double discount = 0.0 ; };
可以为纯虚函数提供定义,但必须定义在类的外部。
含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。不能创建抽象基类的对象。
派生类构造函数只初始化它的直接基类(而不是最顶层的那个):
1 2 3 4 5 6 7 8 class Bulk_quote : public Disc_quote{public : Bulk_quote () = default ; Bulk_quote (const std::string& book, double price, std::size_t qty, double disc): Disc_quote (book, price, qty, disc){} double net_price (std::size_t ) const override ; };
访问控制与继承 protected说明符:
和私有成员类似,受保护的成员对类的用户 不可访问
和公有成员类似,受保护的成员对派生类的成员和友元 可访问
派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Base {protected : int prot_mem; }; class Sneaky : public Base{ friend void clobber (Sneaky&) ; friend void clobber (Base&) ; int j; }; void clobber (Sneaky& s) {s.j = s.prot_mem = 0 ;} void clobber (Base& b) {b.prot_mem = 0 ;}
某个类对其继承来的成员的访问权限受到两个因素的影响:
在基类中该成员的访问说明符
在派生类的派生列表中的访问说明符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Base {public : void pub_mem () ; protected : int prot_mem; private : char priv_mem; }; struct Pub_Derv : public Base{ int f () {return prot_mem;} char g () {return priv_mem;} }; struct Priv_Derv : private Base{ int f1 () const {return prot_mem;} }; Pub_Derv d1; Priv_Derv d2; d1.pub_mem (); d2.pub_mem (); struct Derived_from_Public : public Pub_Derv{ int use_base () {return prot_mem;} }; struct Derived_from_Private : public Priv_Derv{ int use_base () {return prot_mem;} };
严格来说,private成员可以继承,但只能通过内存地址等非常规方式进行访问。 下面举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <iostream> using namespace std;class A { int i; void privateFunc () { cout<<"this is a private function of base class" <<endl; } public : A (){i=5 ;} }; class B :public A {public : void printBaseI () { int * p=reinterpret_cast <int *>(this ); cout<<*p<<endl; } void usePrivateFunction () { void (*func)()=NULL ; _asm { mov eax,A::privateFunc; mov func,eax; } func (); } }; int main () { B b; b.printBaseI (); b.usePrivateFunction (); }
另外补充:书中的某些表述比较模糊,下面给出一个例子进一步说明某个情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> using namespace std; class Base {protected : int i; }; class Derived : public Base{public : void fun (Derived d) { d.i = 3 ; } }; int main () { Derived derived; return 0 ; }
友元关系不能继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Base { friend class Pal ; }; class Pal {public : int f (Base b) {return b.prot_mem;} int f2 (Sneaky s) {return s.j;} int f3 (Sneaky s) {return s.prot_mem;} }; class D2 : public Pal{public : int mem (Base b) { return b.prot_mem; } };
通过使用using改变个别成员的可访问性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Base {public : std::size_t size () const {return n;} protected : std::size_t n; }; class Derived : private Base {public : using Base::size; protected : using Base::n; };
默认的继承保护级别:
1 2 3 class Base {};struct D1 : Base {}; class D2 : Base {};
未完待续 两个未完待续之间的内容并未学习。
https://www.bilibili.com/video/BV1z64y1U7hs?p=79