C++Primer 第五版中文版阅读笔记
第二章 基本内置类型
2.1复合类型
2.2const限定符
const表示一个量的值不能被修改,例如进行如下定义const int buff=512;
留意以下的情况
int i =1;
const int cnt = i;
在VSCode中认为cnt不是一个完整的常量,这是因为认为尽管cnt是一个整型常量,但是它的常量特征只在修改其值时发生作用。下面是一个实例
2.2.1常量与引用
可以将引用绑定到const对象上,这样就是一个对常量的引用,具体声明如下,此时就引用ref而言,他认为自己指向了一个常量,所以它们自觉地不去修改其所指对象的值。与之对应的是,对一个常量引用可能并未指向一个常量。
const int i=1021;
const int &ref =i;
int ci =1;
const int &re = i;
re = 2; //错误,它认为自己指向常量
i=2; //正确,i是一个非常量,可以修改
2.2.2常量与指针
对于常量与指针的关系,要区分的是常量指针和指向常量的指针。常量指针类似于常量引用,认为自己指向的是一个常量;指向常量的指针本身就是一个常量,它的指向是不能再改变的。
int nub=0;
const int *p = &nub; //指向常量的指针
int *const *pi = &nub; //常量指针
const int *const pip = &nub; //指向常量的常量指针
2.2.3顶层const和底层const
对于指针而言;顶层const表示指针本身是个常量,底层const表示指针所指的对象是一个常量。
有下面类似的代码示例,这说明可以用非常量初始化一个底层const,但是反过来是不行的。
int i =24;
//成功赋值部分
const int *cp = &i;
const int &cc = i;
const int &ref = 24;
//类型不匹配赋值失败
int *p = cp;
int &c = cc;
int &r = 24; //不能用字面值初始化非常量引用
第三章 string、vector和数组
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
3.1 string类型
3.1.1 初始化
string类型的初始化分为直接初始化和拷贝初始化;
3.1.2 string上的操作
- 可以留意string与流的操作,如将s写入到流os中(
os<<s
),从流is中读取字符串到s(is>>s
) - string在读取是会自动忽略头部的空白,并在遇到下一个空白时停止,为了弥补这个可以应用到getline读取string,它在遇到换行符时停止。
3.1.3string下标访问
string可以类似数组进行下标访问,其下标类型是string::size_type
类型
3.2.vector类型
3.2.1 初始化
vector的初始化方法比较多,可以通过小括号和中括号进行初始化。一般来说小括号是方法,大括号是值。而且还可以通过数组或者向量进行初始化。
//列表初始化
vector<int> iVec1{1,2,3,4,5};
vector<string> sVec1{"sh","yo","we"};
//值初始化
vector<int>iVec2(10); //10个0
vector<string>sVec2(10); //10个空串
//易混淆的
vector<int>iVec3{10,1}; //两个元素10和1
vector<int>iVec4(10,1); //10个1
vector<string>sVec3{"HI"};
vector<string>sVec3{10}; //10个空串
vector<string>sVec3{10,"HI"}; //10个"HI"
3.2.2
3.3.迭代器
迭代器用来模拟类似于下标的访问;常用的有string与vector支持的迭代器
3.3.1如何使用迭代器
首先来说,需要了解的是迭代器的两个重要的成员(begin和end),需要着重记住的是end成员指向的是尾元素的下一个位置。在C++11的标准中对于迭代器的声明一般使用auto,这样可以避免很多的人为错误。迭代器的运算符操作比较简单这里不再赘述。下面举一个简单使用迭代器的例子
//代码的功能是将字符串s的字母全部变为大写
string s = "hello world";
for(auto it =s.begin();s!=s.end();++it){
*it = toupper(*it)
}
关于迭代器的类型问题:
- 虽然在一般使用中我们可以使用auto来进行迭代器的类型说明,但是还是需要了解到一些具体概念,比如对于int数据类型组成的vector而言,它的迭代器类型就是
vectot<int>::iterator
. - 对于迭代器而言也有着类似于常量指针的常量迭代器,如上述对应的就是
vectot<int>::const_iterator
类型.前者可以通过迭代器进行读写,后者则只能进行读。其实,在理解上我们可以确实将迭代器当作一个指针来操作,就像上面的示例代码中的对s的特定字符的操作就需要用*
号来进行解引用。
#include <iostream>
#include <string>
#include <iomanip>
#include<vector>
using namespace std;
int main(){
vector<string> text;
string s;
while(getline(cin,s)){
text.push_back(s);
for(auto iter = text.begin(); iter!=text.end() && !iter->empty();iter++){
for(auto it = (*iter).begin();it!=iter->end();it++){
*it = toupper(*it);
}
cout<<*iter;
}
}
}
3.3.2迭代器的运算
- 迭代器的运算一般指迭代器的前进后退操作,这一般是通过重载的运算符+和-等实现的,使用比较简单。类似于数值的运算。
- 只要两个迭代器指向的是同一个容器中的元素位置,就能够进行相减得到二者之间的距离,对于这个距离有个专门的类型
difference_type
,注意这是个带符号类型,如以下使用
vector<string>::iterator i1 = src.begin();
vector<string>::iterator i2 = src.end();
vector<string>::difference_type diff = i2 - i1;
下面是一个利用迭代器进行二分查找的C++实例,需要注意的是,利用迭代器直接进行相加操作时不正确的。如我们在常用的二分查找中使用迭代器的话,对于mid的计算是mid=begin+(end-begin)/2;
3.4 数组
数组类似于vector,通过下标进行访问,但是不同的是它不能进行动态元素添加。而且不同于vector的是,数组不可以进行拷贝和复制,也就是不能通过一个数组来初始化另一个数组。如以下代码就是错误的;
在数组的学习中应该理解一下字符数组的特殊性,如果用一个字符串来初始化字符数组,则其最后一个元素是一个空字符。
char a[] = {'C','+','+','\0'}
char a1[3] = "HEL"; //错误,没有空间放空字符'\0'
char a2[] = "HEL"; //{'H','E','L','\0'}
3.4.1指针和数组
提到指针与数组是因为在大多数表达式中,数组类型的对象其实就是一个指向数组首元素的指针。如下面的代码就会输出one two
string nums[] = {"one","two","three"};
string *p=nums;//等价于string *p=&nums[0];
cout<<*p<<" "<<*(p+1)<<endl;
指针其实也是迭代器,提到指针我们就很容易想到迭代器这个类型,其实数组的指针就相当于一个迭代器。迭代器是有首尾元素的,也可以根据这个求出数组的首尾指针,类似的原理就是对于数组的尾指针也是指向最后一个元素的下一个位置(下标不存在的元素)。为了防止我们在自己使用时候出错,C++11引入了类似迭代器首尾元素的两个标准库函数,分别是begin和end。
int ia[]={1,2,3,4,5,6,7,8,9,10};
int *beg = begin(ia); //ia首元素的指针
int *en = end(ia); //ia尾元素的下一个位置指针
while(beg!=en){
cout<<*beg;
beg++;
}
另外不同于迭代器的是,数组元素的指针二者相减的数据类型是ptrdiff_t
我们需要记住的是,当两个指针指向同一个元素(数组或者向量)时,二者就是可以比较的。比如上面代码的第四行就等价于while(beg<en)
- 复杂的数组声明比较难懂从数组的名字开始由内到外顺序阅读(参见原书103页)
3.4.2指针数组和数组指针
int arr[10];
int *p1[10]; //含有十个指针的指针数组
int (*p2)[10]; //指向一个十个数数组的数组指针
3.4.3 C风格字符串
C风格字符串其实就是一个以空字符‘\0’结尾的字符数组。这是C++由C继承而来的。这些字符串的操作一般通过指针实现。C风格字符串其实就是一个常量字符数组,如下面代码的第一行声明,其中数组名就是一个指向首元素的指针,也就是一个const char *
的指针。
const char ca2[] = "a c-style string";
const char ca2[] = "a good c-style string";
if(strcmp(ca1,ca2)<0){
}
为了方便与以前旧代码的结合,C++中提供了c_str
函数,可以实现字符串转换为C风格字符串。
string s = "Hello World";
const char *str = s.c_str();
3.4.4 数组可以用来初始化vector
需要注意的是,数组不可以初始化数组,向量也不能初始化数组。
int ia[]={1,2,3,4,5,6,7,8,9,10};
vector<int>ivec (begin(ia),end(ia));
3.4.5 多维数组
将二维数组理解为数组的数组,高维继续类推就可以了。
- 多维数组的下标引用
只要是数组就可以采用下标进行访问,但是如果只用一个下标运算符的话就会出现不同的结果,正如我们前面提到的,对数组元素的访问其实都是通过指针进行的,所以使用下标就有以下两种情况:
-下标运算符数量和数组维度一样,访问的就是数组元素
-下标运算符少于数组维度,访问的就是指定索引处的一个内层数组。
下面是一个代码示例
int a1[2][3]={1,2,3,4,5,6};
int (&row)[3] = a1[0]; //此时的row就是绑定了a1的第一个三元素数组
cout<<row[1]<<endl; //输出2
cout<<*(row+2)<<endl;//输出3
int a2[3][3][3]={7,8,9,10,11,12,13,14,15,16,18};
//如果外层不是引用,此时的row就是一个指向第一个二维数组首元素的指针(int *)
for(auto &row :a2){
for(auto &col:row){
for(auto &vin:col)
cout<<vin<<" ";
cout<<endl;
}
}
- 指针和多维数组
多维数组与指针的关系类似于一维数组与指针的关系,通过类推我们可以得出,指向多维数组的指针实际上是一个指向数组的指针。
int a1[3][4] = {1,2,3,4,5,6,7,8};
int (*p)[4] = a1; //是一个指向数组的指针
int *p[4]; //是一个指针数组
//前者是第一个元素的地址,后者是第一个元素的实际值
cout<<*p<<**p<<endl;
下面是采用三种方法进行一个二维数组遍历的代码
int a1[3][4] = {1,2,3,4,5,6,7,8};
cout<<"第一次"<<endl;
for(auto p=a1;p!=a1+3;p++){
for(auto q=*p;q!=*p+4;++q)
cout<<*q<<" ";
cout<<endl;
}
cout<<"第二次"<<endl;
for(auto p=begin(a1);p!=end(a1);++p){
for(auto q=*p;q!=end(*p);++q)
cout<<*q<<" ";
cout<<endl;
}
cout<<"第三次"<<endl;
using int_a = int[4];
//typedef int int_a[4];
for(int_a *p =a1;p!=a1+3;++p){
for(int *q =*p;q!=*p+4;++q){
cout<<*q<<" ";
}cout<<endl;
}
第四章 表达式
4.1求值顺序
C++标准中只规定了少数的二元运算符的求值顺序(逻辑与&&、逻辑或||、逗号运算符和条件运算符?:都是从左到右,先计算左侧),对于其他大多数运算符来说并没有明确规定,这在某种程度上加快了代码生成的效率,但是有些时候会引发潜在的危险。
所以在同一个表达式中如果有两个相同的对象,那么就会引发未定义的行为。如代码int i=0;cout<<i<<" "<<++i<<endl;
的输出结果可能是1 1
,也可能是0 1
等等.所以我们在复合表达式的书写中可以注意:
int factorial(int x){
if (x>1){
return factorial(x-1)*factorial(x);
}
else return 1;
}
4.2sizeof运算符
sizeof运算符用来求一个值的所占字节数;比较容易混淆的是sizeof对于数组的大小操作。注意如果对数组名求sizeof,此时求值结果是整个数组的大小,不会将数组名转换成指针来处理。
vector<int> iVec1{1,2,3,5,6,7,8,9,0,0,11};
vector<int> iVec2{1,2,3,};
vector<string> sVec1{"sh","yo","we"};
cout<<"iVec1: "<<sizeof(iVec1)<<" iVec2: "<<sizeof(iVec1)<<" sVec1: "<<sizeof(sVec1)<<endl; //输出均为8,运算符不求vector的实际大小
int x[10];
int *p = x;
cout<<sizeof(x)/sizeof(*x)<<endl; //此时的x是对数组的大小求值
cout<<sizeof(p)/sizeof(*p)<<endl;
**cout<<sizeof("")<<endl; //返回结果为1**
4.3类型转换
计算机中算术类型的存储是补码形式;
4.3.1算术转换
- 整型提升
就是说一些小整数类型在应用时会转向大整数类型。具体有两种情况,一是对于bool,char
等类型来说,在运算时如果它们可能的值在int
里,就会转换成int
,否则继续提升到unsigned int
;二是对于较大的char
类型来说提升到int、unsigned int
等中最小的一种类型,前提是提升后的类型能够容纳原类型所有可能的值。 - 涉及无符号类型转换
无符号类型参加运算时转换的结果依赖于各个整数类型的相对大小。一般是小类型的运算对象转换成较大的类型。但是如果无符号类型是较小的类型,此时就要根据二者所占的空间进行转换,一般也是小到大。这有时会导致错误。可以看下面的代码输出。 - 一个例子
int a = -101;
unsigned int b = 100;
cout<<a+b<<" "<<endl; //输出不是-1,因为此时的运算结果转换为了unsigned int
int ival;
unsignedshort usval;
unsigned int uival;
long lval;
ival+uival; //转成unsigned int
usval+ival; //根据unsigned short和int所占的空间大小转换,只有int更多才转成int
uival+lval; //根据unsigned int和long所占的空间大小转换,只有long更多才转成int
4.3.2其他隐式转换
- 数组指针的隐式转换
在使用数组时一般会默认将数组名转换成指向数组的指针,但是有些情况例外(如sizeof
运算符,取地址符&
以及typeid
等) - 转换成常量
在常量初始化时有时候允许用非常量来初始化,但是相反就不行。下面给出的示例代码中后两句注释的会提示出错,因为此时试图删除掉底层const
int i=99;
const int j=100;
const int &r = i;
const int *p = &i;
//int &tr = r;
//int *re = &j;
- 类类型定义的转换
4.3.3显式转换
显式转换也叫强制类型转换,这种方法是很危险的。强制类型转换的一般语法结构是cast-name<type>(expression)
其中cast-name
是转换类型的类别,主要有以下四种
- static_cast:只要不包含底层const都可以通过这个类型进行类型转换。
- const_cast:可以用来改变运算对象的底层const资格;也就是说这个形式的强制类型转换可以改变表达式的常量属性,可以使得常量可以被修改。但是const_cast不能修改变量的类型。
- reinterpret_cast:风险很大,尽量避免使用
- dynamic_cast:支持运行时类型识别
4.4 左值和右值
P121
第五章 语句
5.1 异常处理
5.1.1 一般概念
异常是指程序的某部分检测到它无法处理的问题时,就会用到异常处理。专门的异常会有专门的处理。C++的异常处理机制是通过三个部分完成的,分别是throw表达式、try语句块、以及异常处理代码。
一般来说异常处理使用的是try语句块:
try{
programe-statements
throw;
}catch(exception-declaration){
handler-statements
}
- 其中try语句中的大括号中的
programe-statements
是加入监测的程序代码;exceptiton-declaration
是异常声明,一般是指明是何种异常;handler-statements
是异常类型匹配成功后进行处理的语句。一般来说一个try语句后可以跟多个catch语句,也就是说可以对一段程序的异常进行多种类型的检测。 - 注意try语句中不要忘记**
throw
** - 在正常的大型程序中
try
语句是可以嵌套的。在嵌套的try
语句中必然有着嵌套的多个catch
语句。
5.1.2异常处理中函数的退出
- 考虑上面所说的
try
语句嵌套的情况,当异常发生时,首先要搜索相匹配的catch
语句进行异常处理,此时如果未找到相应的catch
处理语句,那么就要在调用这个try
语句块的函数中寻找最近的catch
语句块,如果最终没有找到catch
语句,那么程序会执行中止函数的操作,其实也就是一个名为terminate
的标准库函数。总的来说就是沿着程序执行路径逐层回退寻找catch
语句。
- 我们需要再理解一个概念:那就是函数在寻找异常处理代码的过程中退出
不难想到,一旦发生异常,那么try语句块中的代码一般是未执行完的,也就是说异常中断了程序的正常流程。也就是一部分完成,一部分未完成,要达到异常安全的程序是十分困难的。在对于异常发生后还要继续执行的程序而言,我们必须考虑到如何保证后面运行的程序安全有效、资源不泄露、对象有效等。
5.1.3 标准异常类
异常类一般会提供异常出错信息
- exception头文件定义了最通用的异常类exception,只报告异常的发生,不提供任何额外信息
- stdexception头文件定义了下面表1几种常用的异常类
- new头文件定义了bad_alloc异常类型
- type_info 定义了bad_cast异常类型
表1
异常类名 | 异常说明 |
---|---|
exception | 最常见的问题 |
runtime_error | 运行时才能检测到的错误 |
range_error | 运算生成的结果超出了有意义的范围 |
overflow_error | 计算下溢出 |
underflow_error | 计算上溢出 |
logic_error | 程序逻辑错误 |
domain_error | 参数对应的结果值不存在 |
invalid_argument | 无效参数 |
length_error | 视图创建一个超出数据类型最大长度的对象 |
out_of_range | 使用一个超出范围的值 |
下面是一个异常处理的例子:
int main(){
int m,n;
while(cin>>m>>n){
try{
if(n==0){
throw runtime_error("除数不能为0");
}
cout<<"运行结果是:"<<m/n<<endl;
}catch(runtime_error err){
cerr<<err.what()<<endl;
cout<<"需要继续吗?(y or n)"<<endl;
char ch;
cin>>ch;
if(ch != 'y' && ch!='Y'){
break;
}
}
}
return 0;
}
第六章 函数
6.1 函数基础
6.1.1 易混淆概念
- 形参和实参必须相匹配(类型可以进行最终匹配、数量不能多)
- 函数三要素:返回类型、形参类型和函数名
6.1.2main函数深究
- main函数其实是一个特殊的函数,它的返回类型为int型,但是我们在实际过程中又可以省略后面的
return 0;
,这是因为如果控制到了main的结尾处而没有此语句,编译器会隐式执行该语句。一般来说,main
函数返回0表示程序成功退出;返回值是其他非零值的话具体含义依据机器决定。 - main函数的参数可以为空,也可以为
int main(int argc, char *argv[])
;其中argc是命令行总的参数个数,argv
[]是argc
个参数数组,其中第0个参数argv[0]
是程序名,其后的参数是命令行后面跟的用户输入的参数,下面是一个简单示例:
int main(int argc, char **argv){
int i;
for(i = 0;i<argc;i++){
cout<<argv[i]<<endl;
}
return 0;
}
6.2 参数传递
引用传递和值传递辨析:
- 值传递:形参的值是由实参拷贝而来,是两个独立的对象。也就是传值调用;
- 引用传递:形参是引用类型,是将实参与其绑定,引用形参是实参的别名。也就是传引用调用
经典的swap函数的三种实现可以更有助于理解
void SWAP(int p1,int p2);
void SWAP(int &p1,int &p2)
void SWAP(int *p1,int *p2)
6.2.1 引用传递的优点
- 不必拷贝实参的值,节省空间;
- 可以直接操作实参对象;
- 可以帮助实现返回多个值
所以分析为什么使用引用传递或者不使用引用传递应该从以上三方面进行分别考虑。
6.2.2 常量引用形参
在使用const形参时可以对比前面第2章的顶层const
和底层const
做对比。如果有一个reset(int &i)
函数,参考2.2.3的代码段可以得出下面的三条调用都是指针类型出错的。
void reset(int &i);
int i =24;
int &ref = i;
const int *cp = &i;
const int &cc = i;
const int &ref = 24;
reset(cp); //错误,const int *cp是一个底层const,不能转换为int *
reset(cc); //错误,const int &cc是一个底层const,不能转换为int &
reset(24); //错误,不能将字面值绑定到普通引用上
reset(i); //正确,int类型可以转换为int &,注意非常量引用的形参初始值必须为左值
reset(ref) //正确
非常量引用的形参初始值必须为左值或者其已定义的引用,常量引用的初始值可以是字面值,所以对于引用调用的写法时reset(i)
而不是reset(&i)
,这是因为&i是一个int *
类型的参数,在函数的参数匹配中int *
和int &
不兼容。具体来讲,以SWAP()
函数来讲,要调用引用版本的时候,只能使用int
类型的对象;要调用指针版本的时候则只能使用int *
6.2.3 尽量使用常量引用
这样的话不论实参是常量值还是非常量值都是可行的,出于设计的考虑这是比较科学的。
6.2.4 数组形参
前面就介绍过,在数组实际使用中,其实就相当于将数组名看作一个指向首元素的指针,所以对于数组作为函数参数,就只是首元素指针的值传递,因为其本身就是一个指针。数组做形参有三种形式,分别是:
void operation(const int *cp){}
void operation(const int a[10]){} //10只是我们期望的维度,实际调用中不一定为10,可以自己实践尝试
void operation(const int ia[]){}
一旦使用指针就必须要注意边界情况,在数组元素的边界确定时,主要使用三种方法来管理
- 遇空指针停止
- 首尾元素指针
begin
和end
(尾元素的下一个位置)进行控制 - 确定范围值做循环控制
下面是一个设计指针形参的代码
//交换两个指针本身
void swapPoint(int *&p1,int *&p2){
int *t=p1;
p1=p2;
p2=t;
}
//交换指针所指的值
void swapPointV(int *p1,int *p2){
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
6.2.5 可变形参
主要是新标准中用于不确定函数参数的声明;
6.3 retuen语句与函数返回值
函数一般都是要执行return用作返回值的,但是void
类型函数可以隐式执行return;
。return可以执行返回类型检查;
6.3.1 值如何被返回?
执行return后,返回值根据返回类型到达函数调用点(如果不是返回引用则需要拷贝回去)。
6.3.2 引用返回左值
就是说函数返回值返回引用时,得到的是左值,可以像使用其它左值一样返回引用函数的调用;
char &get_Val(string &str,int ix){
return str[ix];
}
const char &get_Const_Val(string &str,int ix){
return str[ix];
}
int main(){
string s1 = "Hello";
get_Val(s1,2) = 'p';
get_Const_Val(s1,1) = 'p'; //错误 不能修改常量
cout<<s1<<endl;
}
6.3.3 返回数组指针/引用
函数声明形式:Type (*function(Paramer_list))[dimension]
,具体例子如下:
int arr[]={1,2,3,4,5,6};
int (*function(int i,int j))[10];
auto function(int i)->int (*)[10];//函数接受一个实参i,返回一个指向含有十个整数数组的指针
decltype(arr) *arrPtr(int i);
6.4 函数重载
定义:除了形参不同,其他的都相同(函数名)。编译器会根据形参类型在调用时确定调用的是哪一个。注意其中形参不同的定义。实际中是否使用函数重载取决于重载后的函数调用是否更方便,如果不能则不建议进行函数重载。
//形参类型项相同不是重载
int get();
double get();
//形参类型不同,是重载
int *reset(int *);
double *reset(double *);
思考:形参顺序不一样算重载吗???
6.4.1 const形参和重载
- 这里再次提到我们前面讲的底层和顶层const的应用。我们需要记住的是,C++在类型转换的时候对于底层const是比较严格的,也就是说如果存在两个函数参数类型数量都相同,但是一个是底层const,那么还是算重载函数的。但是如果是顶层const的话因为参数是无法区分的所以不算重载。如下面代码:
void printh(int j){
}
//不是函数printh的重载,重复声明错误;顶层const无法区分
void printh(const int j){
}
void printI(int *i){
}
//不是函数printI的重载,重复声明错误;顶层const无法区分
void printI( int* const i){
}
- 下面的函数重载就是正确的
void printh(int &i){
}
void printh(const int &j){
}
void printI(int *i){
}
void printI(const int *i){
}
6.4.2 const_cast形参和重载
前面(4.3.3)已经在类型转换里见过static_cast
,const_cast
等等,这里所说的const_cast
形参其实就是对形参进行强制类型转换。
所以说有时候加了const修饰不会构成重载,但是有时候会,就是上面所说的类型发生变化的情况。可以看书中给出的下面的函数重载的例子,这个重载可以针对实参是常量还是非常量进行选择调用。当我们应用了类似的例子,常量调用函数时就会返回常量的引用,非常量调用函数就会返回非常量引用。其实我们可以想象如果一个是常量一个是非常量的话那么会选择哪个重载函数调用呢?这确实有点难以界定,在处理重载函数形参数量相同且可以相互转换的情况下确实比较难以区分函数调用存在类型转换时会调用哪个重载函数,这一细节会在函数匹配6.6小节介绍。
const string &shorterD(const string &s1,const string &s2){
return s1.size()<s2.size()?s1:s2;
}
//注意这两个函数构成重载函数的情况(返回类型不同、形参类型不同)
string &shorterD(string &s1,string s2){
const string &r = shorterD(const_cast<const string &>(s1),const_cast<const string &>(s2));
return const_cast<string &>(r);
}
6.4.2 作用域和重载
不同的作用域中无法重载函数名
编译器处理一个名称时会首先在距离最近的作用域寻找,就是说如果有局部作用域一般会首先在局部作用域寻找。