4 表达式
4.1 基础
1、左值和右值:C中原意:左值可以在表达式左边,右值不能。 C++:当一个对象被用作右值的时候,用的是对象的值(内容), 被用做左值时,用的是对象的身份(在内存中的位置)。
2、优先级和结合律:括号无视优先级和结合律。
3、求值顺序:int i = f1() + f2()
,先计算f1() + f2()
,再计算int i = f1() + f2()
。但是f1和f2的计算先后不确定,但是,如果f1、f2都对同一对象进行了修改,因为顺序不确定,所以会编译出错,显示未定义。
4.2 运算符
4.2.1 算数运算符
1、溢出:当计算的结果超出该类型所能表示的范围时就会产生溢出。
2、bool类型不应该参与计算: 如bool b1 = true;
但bool b2 = -b1;
的b1不可能为-1,其值还是true。
3、取余运算m%n,结果符号与m相同
4.2.2 逻辑运算符
1、短路求值:逻辑与运算符和逻辑或运算符都是先求左侧运算对象的值再求右侧运算对象的值,当且仅当左侧运算对象无法确定表达式的结果时才会计算右侧运算对象的值。先左再右
2、声明为引用类型可以避免对元素的拷贝,如下,如string特别大时可以节省大量时间。
vector<string> text;
for(const auto &s: text){
cout<<s;
}
4.2.3 赋值运算符
1、 赋值运算的返回结果时它的左侧运算对象,且是一个左值。类型也就是左侧对象的类型。
2、 如果赋值运算的左右侧运算对象类型不同,则右侧运算对象将转换成左侧运算对象的类型。
3、赋值运算符满足右结合律,这点和其他二元运算符不一样。 ival = jval = 0;
等价于ival = (jval = 0);
4、赋值运算优先级比较低,使用其当条件时应该加括号。
5、复合赋值运算符,复合运算符只求值一次,普通运算符求值两次。任意复合运算符op等价于a = a op b;
,其中op有+=、-=、*=、/=、%=等
4.2.4 递增递减运算符
1、前置版本j = ++i
,先加一后赋值
2、后置版本j = i++
,先赋值后加一
3、优先使用前置版本,后置多一步储存原始值。(除非需要变化前的值)
4、混用解引用和递增运算符:*iter++
等价于*(iter++)
,递增优先级较高。
auto iter = vi.begin();
while (iter!=vi.end()&&*iter>=0)
cout<<*iter++<<endl; // 输出当前值,指针向前移1
4.2.5 成员访问、条件和位运算符
1、成员访问:ptr->mem
等价于(*ptr).mem
,注意.
运算符优先级大于*
,所以记得加括号
2、条件运算符 :(?:
)允许我们把简单的if-else
逻辑嵌入到单个表达式中去,按照如下形式:cond? expr1: expr2
,可以嵌套使用,右结合律,从右向左顺序组合,输出表达式使用条件运算符记得加括号,条件运算符优先级太低。
3、位运算符:位运算符是作用于整数类型的运算对象。 二进制位向左移(<<
)或者向右移(>>
),移出边界外的位就被舍弃掉了。 位取反(~
)(逐位求反)、与(&
)、或(|
)、异或(^
),一般情况下,位运算符仅用于无符号数。
4.2.6 sizeof运算符
1、返回一条表达式或一个类型名字所占的字节数。返回的类型是 size_t
的常量表达式。 sizeof
并不实际计算其运算对象的值。
2、两种形式: sizeof (type)
,给出类型名和 sizeof expr
,给出表达式
3、可用sizeof返回数组的大小,如int ia[10],利用sizeof(ia)返回整个数组所占空间的大小,sizeof(ia)/sizeof(*ia)返回数组的大小
。
4.2.7 逗号运算符
从左向右依次求值。左侧求值结果丢弃,逗号运算符结果是右侧表达式的值,逗号运算符常常用在for循环当中。
4.3 类型转换
4.3.1 隐式转换
1、目的:为了尽可能避免损失精度,编译器自动的将变量转换为更精细类型。
2、转换规律:①比 int
类型小的整数值先提升为较大的整数类型。②条件中,非布尔转换成布尔。③初始化中,初始值转换成变量的类型。④算术运算或者关系运算的运算对象有多种类型,要转换成同一种类型。⑤ 函数调用时也会有转换。
3、整型提升 :常见的char、bool、short能存在int就会转换成int,否则提升为unsigned int
, wchar_t,char16_t,char32_t
提升为整型中int,long,long long ……
最小的,且能容纳原类型所有可能值的类型。
4.3.2 显式转换
1、static_cast:任何明确定义的类型转换,只要不包含底层const,都可以使用。 double slope = static_cast<double>(j);
2、dynamic_cast:支持运行时类型识别。
3、const_cast:只能改变运算对象的底层const,一般可用于去除const性质。 const char *pc; char *p = const_cast<char*>(pc)
,只有它可以改变常量属性。
4、reinterpret_cast:通常为运算对象的位模式提供低层次上的重新解释。
5、旧式强制类型转换 :type expr
,例如int(3.14)
,等等。
4.4 运算符优先级表
5 语句
5.1 简单语句
1、表达式语句:一个表达式末尾加上分号,就变成了表达式语句。
2、空语句:只有一个单独的分号。
3、复合语句(块):用花括号 {}
包裹起来的语句和声明的序列。一个块就是一个作用域。
5.2 跳转语句
1、break:break
语句负责终止离它最近的while
、do while
、for
或者switch
语句,并从这些语句之后的第一条语句开始继续执行。
2、continue:终止最近的循环中的当前迭代并立即开始下一次迭代。只能在while
、do while
、for
循环的内部。
5.3 try语句块和异常处理
1、throw表达式:异常检测部分使用 throw
表达式来表示它遇到了无法处理的问题。我们说 throw
引发 raise
了异常。
2、try语句块:以 try
关键词开始,以一个或多个 catch
字句结束。 try
语句块中的代码抛出的异常通常会被某个 catch
捕获并处理。 catch
子句也被称为异常处理代码。
3、异常类:用于在 throw
表达式和相关的 catch
子句之间传递异常的具体信息。
6 函数
6.1 函数基础
6.1.1 局部对象
1、实参和形参: 实参是形参的初始值。第一个实参初始化第一个形参,第二个实参初始化第二个形参,依次类推。
2、局部变量: 形参和函数体内部定义的变量统称为局部变量(localvariable),仅在函数的作用域内可见,同时局部变量还会隐藏(hide)在外层作用域中同名的其他所有声明中。
3、自动对象: 象我们把只存在于块执行期间的对象称为自动对象
(automatic object)。当块的执行结束后,块中创建的自动对象的值就变成未定义的了。形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
4、局部静态对象(static):某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成static
类型从而获得这样的对象。局部静态对象( local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。例如统计函数调用多少次:
#include<iostream>
using namespace std;
int count_static();
int main()
{
for(int temp = 0; temp < 10; temp++){
cout<<count_static();
}
return 0;
}
int count_static(){
static int cnt;//声明为static对象
cnt++;
return cnt;
}
6.1.2 函数声明和分离式编译
1、函数声明:函数的声明和定义唯一的区别是声明无需函数体,用一个分号替代。函数声明主要用于描述函数的接口,也称函数原型。
2、在头文件中进行函数声明:建议变量在头文件中声明;在源文件中定义。
6.2 参数传递
如前所述,每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。形参初始化的机理与变量初始化一样。
和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上,否则,将实参的值拷贝后赋给形参。
- 当形参是引用类型时,我们说它对应的实参被引用传递( passed by reference)或者函数被传引用调用(called by reference)。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
- 当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被值传递(passed by value)或者函数被传值调用( called by value)。
6.2.1 传值参数(值传递/值拷贝)
1、当初始化一个非引用类型的变量时,初始值被拷贝给变量。函数对形参做的所有操作都不会影响实参。
2、比较常见,一般通过返回值来承接变量变化后的值。
6.2.2 指针参数
指针的行为和其他非引用类型一样(指针也是一种数据类型,或者参考指针和引用的区别,这句话的目的是说,虽然指针也是个这种数据类型,但是指针却能够修改指向对象的值)。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以**通过指针可以修改它所指对象的值**。
#include<iostream>
using namespace std;
// let the value of n or p to be zero 0
int value_function(int n);
int point_function(int* p);
int reference_function(int& r);
int main()
{
int temp = 1;
value_function(temp);
cout<<"the value of temp is: "<<temp<<endl;
cout<<"the value of value_function(temp) is: "<<value_function(temp)<<endl;
int* p = &temp;
cout<<"the value of *p before function is: "<<*p<<endl;
point_function(p);
cout<<"the value of *p after function is: "<<temp<<endl;
int re = 1;
int& r = re;
cout<<"the value of &r before function is: "<<r<<endl;
reference_function(r);
cout<<"the value of &r after function is: "<<r<<endl;
return 0;
}
int value_function(int n){
n = 0;
return n;
}
int point_function(int* p){
*p = 0;
return *p;
}
int reference_function(int& r){
r = 0;
return r;
}
输出结果为the value of temp is: 1
the value of value_function(temp) is: 0
the value of *p before function is: 1
the value of *p after function is: 0
the value of &r before function is: 1
the value of &r after function is: 0
6.2.3 传引用参数
1、使用引用避免拷贝:当某种类型不支持拷贝操作时(如IO类型),函数只能通过引用形参访问该类型的对象。如果无需改变引用形参的值,最好将其声明为常量引用。
2、使用引用形参返回额外信息:一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。
6.2.4 const形参和实参
1、顶层const形参的拷贝属性:顶层const作用于对象本身。和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。即当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
//顶层const
const int ci = 42; //顶层const,被修饰的值无法改变即ci =43是错误的
int i = ci; //拷贝ci时候忽略他的顶层const属性
i = 43;
int* const p = &i; //const是顶层的,指针常量,指针是常量类型的
*p = 44; //合理,通过指针修改值
2、指针和引用形参与const:形参的初始化方式和变量的初始化方式是一样的,例如我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化,尽量使用常量引用。
6.2.5 数组形参
1、数组的两个特殊性质:①不允许拷贝数组;②使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用
数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
2、数组形参的形式:尽管表现形式不同,但下面的三个函数是等价的:每个函数的唯一形参都是const int*
类型的。
void print (const int*) ;
void print (const int[]) ; //可以看出来,函数的意图是作用于一个数组
void print (const int [10]) ; //这里的维度表示我们期望数组含有多少元素,实际不一定
3、管理数组:和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
- 使用标记管理数组实参,要求数组本身包含一个结束标记,例如C风格字符串存储在字符数组中,并且在最后一个字符后面跟着一个空字符。函数在处理C风格字符串时遇到空字符停止。
void print (const char *cp)
{
if (cp) //若cp不是一个空指针
while (*cp) //只要指针所指的字符不是空字符
cout << *cp++; //输出当前字符并将指针向前移动一个位置
}
- 使用标准库规范:传递指向数组首位元素的指针。
void print (const int *beg, const int *end){
//输出beg到end之间(不含end)的所有元素.
while (beg != end)
cout << *beg++ << endl; //输出当前元素并将指针向前移动一个位置
}
- 显式传递一个表示数组大小的数:专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。
void print (const int ia[], int size)
4、数组形参和const:当函数不需要对数组元素执行写操作的时候,数组
形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
5、数组引用形参:与定义数组的引用一样,数组的引用格式是(&数组名)[长度]
,而&数组名[长度]
表示定义的是引用类型的数组。
6、传递多维数组(即传递一个指针类型的数组): 和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针,因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略。
// matrix 指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix) [10],introwSize) { /* ...*/ }
/*再次注意:再一次强调,*matrix 两端的括号必不可少:
int *matrix[10] ;//10个指针构成的数组
int (*matrix) [10] ;//指向含有10个整数的数组的指针*/
6.2.6 main:处理命令行的选项
对于int main(int argc, char* agrv[])
,对此进行分析:第一个形参argc
表示数组中字符串的数量,第二个形参argv是一个数组,它的元素是指向C风格字符串的指针,因为第二个形参是数组,所以main函数也可以定义成:int main(int argc, char** agrv)
。当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为0。以prog -d -o ofile data0
命令行为例,argc应该等于5,argv应该包含如下的C风格字符串:
argv[0] = "prog"; // 或者argv[0]也可以指向一个空字符串
argv[1] = "-d";
argv[2] = "-0";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
当使用argv中的实参时,一定要记得可选的实参从argv[1]开始,argv[0]保存程序的名字,而非用户输入。
6.2.7 含有可变形参的函数
为了编写能处理不同数量实参的函数,C++11 新标准提供了两种主要的方法:①如果所有的实参类型相同,可以传递一个名为initializer_ list的标准库类型,②如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板。
1、initializer_ list形参:如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list 类型的形参。initializer_list 是一种标准库类型,用于表示某种特定类型的值的数组。initializer_ list类型定义在同名的头文件中,它提供的操作如下所示。
和vector一样,initializer_list 也是一种模板类型,定义initializer_ list 对象时,必须说明列表中所含元素的类型:
initializer_list<string> ls; // initializer_list的元素类型是string
initializer_list<int> li;// initializer_ list的元素类型是int
和vector不一样的是,initializer_ list 对象中的元素永远是常量值,我们无法改变initializer_ list 对象中元素的值。
6.3 返回类型和return语句
1、值是如何被返回的:返回一个值的方式和初始化一个变量或形参的方式完全一样: 返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
2、不要返回局部对象的引用或指针:函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域。
3、引用返回左值:调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值:
char &get_ val (string &str, string: :size_ type ix){
return str [ix] ; // get_ val假定索引值是 有效的
}
int main(){
string s("a value") ;
cout<<s<<endl;//输出a value
get_ val(s,0) = 'A' ;//将s[0]的值改为A
cout<<s<<endl;//输出A value
return 0;
}
4、列表初始化返回值: C++11新标准规定,函数可以返回花括号包围的值的列表。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。
vector<string> process ()
{
// expected和actual是string对象
if (expected.empty())
return{};// 返回一个空 vector对象
else if (expected == actual)
return {"functionX", "okay"}; //返回列表初始化的vector对象
else
return { "functionX", expected, actual};
}
6.3.1 返回数组指针
1、声明一个返回数组指针的函数:首先必须牢记数组的维度;如
int arr[10] ;// arr是一个含有10个整数的数组
int *p1[10] ;//p1是一个含有10个指针的数组
int (*p2) [10] = &arr;//p2是一个指针,它指向含有10个整数的数组
和这些声明一样, 如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:
Type (*function (parameter_list)) [dimension]
,类似于其他数组的声明,Type 表示元素的类型,dimension 表示数组的大小。(*function(parameter_list)
两端的括号必须存在,就像我们定义p2时两端必须有括号一样。如果没有这对括号,函数的返回类型将是指针的数组。
举个具体点的例子,下面这个func函数的声明没有使用类型别名:
int (*func(int i) [10] ;
可以按照以下的顺序来逐层理解该声明的含义:
func(int i)
表示调用func函数时需要一个int类型的实参。(*func(int i))
意味着我们可以对函数调用的结果执行解引用操作。(*func(int i)) [10]
表示解引用func的调用将得到一个大小是10的数组.int (*func(int i)) [10]
表示数组中的元素是int类型。
2、使用尾置返回值类型:在C++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个->
符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i)->int(*) [10] ;
因为我们把函数的返回类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针,并且该指针指向了含有10 个整数的数组。
3、使用decltype:如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型。例如,下面的函数返回一个指针,该指针根据参数 i 的不同指向两个已知数组中的某一个:
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
//返回一个指针,该指针指向含有5个整数的数组
decltype (odd) *arrPtr(int i)
{
return(i%2) ? &odd : &even;//返回一个指向数组的指针
}
arrPtr使用关键字decltype表示它的返回类型是个指针,并且该指针所指的对象与odd的类型一致。因为odd是数组,所以arrPtr返回一个指向含有5个整数的数组的指针。有一个地方需要注意:decltype 并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,要想表示arrPtr返回指针还必须在函数声明时加一个*符号。
6.4 函数重载
1、函数重载:如果同一作用域内的几个 函数名字相同但形参列表不同 ,我们称之为重载(overloaded) 函数。其出现的目的是:函数的名字仅仅是让编译器知道它调用的是哪个函数,而函数重载可以在一定程度上减轻程序员起名字、记名字的负担。
2、main函数不能重载。
6.4.1 重载和const
重载和const形参:顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来:
Record lookup (Phone) ;
Record lookup (const Phone) ;//重复声明了Record lookup (Phone)
Record lookup (Phone*) ;
Record lookup (Phone* const) ;//重复声明了Record lookup (Phone*)
如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:
//对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
//定义了4个独立的重载函数
Record lookup (Account&) ;//函数作用于Account的引用
Record lookup (const Account&) ;//新函数,作用于常量引用
Record lookup (Account*) ;//新函数,作用于指向Account的指针
Record lookup (const Account*) ;//新函数,作用于指向常量的指针
因为const不能转换成其他类型,所以我们只能把const对象(或指向
const的指针)传递给const形参。相反的,因为非常量可以转换成const,所以上面的4个函数都能作用于非常量对象或者指向非常量对象的指针。不过,当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
6.4.2 重载与作用阈
重载对作用域的一般性质 并没有什么改变:如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名:
string read() ;
void print (const string &) ;
void print (double); // 重载print函数
void fooBar (int ival)
{
bool read = false; // 新作用域:隐藏了外层的read
string s = read() ;//错误: read是一个布尔值,而非函数
//不好的习惯:通常来说,在局部作用域中声明函数不是一个好的选择
void print (int) ;//新作用城:隐藏了之前的print
print ("Value: ") ; //错误: print (const string &被隐藏掉了
print (ival) ;//正确:当前print(int)可见
print(3.14) ;//正确:调用print (int); print (double)被隐藏掉了
}
6.5 特殊用途语言特性
1、默认实参:例如 string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
一旦某个形参被赋予了默认值,那么它之后的形参都必须要有默认值。
2、内联(inline)函数:普通函数的缺点:调用函数比求解等价表达式要慢得多. inline函数可以避免函数调用的开销,可以让编译器在编译时内联地展开该函数。inline函数应该在头文件中定义。
3、constexpr函数:指能用于常量表达式的函数。 constexpr int new_sz() {return 42;}
函数的返回类型及所有形参类型都要是字面值类型。constexpr函数应该在头文件中定义。
6.6函数匹配
重载函数匹配的三个步骤:1.候选函数;2.可行函数;3.寻找最佳匹配。
1、候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。
2、可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数(viable function)。
3、寻找最佳匹配:基本思想:实参类型和形参类型越接近,它们匹配地越好。
6.7 函数指针
1、 函数指针:是指向函数的指针。 bool (*pf)(const string &, const string &);
注:两端的括号不可少。
2、函数指针形参:形参中使用函数定义或者函数指针定义效果一样。使用类型别名或者decltype。
3、返回指向函数的指针:1.类型别名;2.尾置返回类型。