const表示一个量的值不能被修改,例如进行如下定义const int buff=512;
留意以下的情况
int i =1;
const int cnt = i;
在VSCode中认为cnt不是一个完整的常量,这是因为认为尽管cnt是一个整型常量,但是它的常量特征只在修改其值时发生作用。下面是一个实例
可以将引用绑定到const对象上,这样就是一个对常量的引用,具体声明如下,此时就引用ref而言,他认为自己指向了一个常量,所以它们自觉地不去修改其所指对象的值。与之对应的是,对一个常量引用可能并未指向一个常量。
const int i=1021;
const int &ref =i;
int ci =1;
const int &re = i;
re = 2; //错误,它认为自己指向常量
i=2; //正确,i是一个非常量,可以修改
对于常量与指针的关系,要区分的是常量指针和指向常量的指针。常量指针类似于常量引用,认为自己指向的是一个常量;指向常量的指针本身就是一个常量,它的指向是不能再改变的。
int nub=0;
const int *p = &nub; //指向常量的指针
int *const pi = &nub; //常量指针,指向不能变化
const int *const pip = &nub; //指向常量的常量指针
从右向左读,读的时候看const距离谁最近;
对于指针而言;顶层const表示指针本身是个常量,底层const表示指针所指的对象是一个常量。
- 当执行对象的拷贝操作时,拷入和拷出的对象必须有着相同的底层const资格。或者二者的数据类型可以转换,一般非常量可以转换为常量,反之则不行。
- 在函数中,顶层 const 不影响传入函数的对象,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开(因为二者可以进行类型转换),而底层const不同的话无法进行类型转换所以被看作两个类型
其实这里也就是是否可以用一种const来初始化另一种const的判断
有下面类似的代码示例,这说明可以用非常量初始化一个底层const,但是反过来是不行的。
int i =24;
//成功赋值部分
const int *cp = &i;
const int &cc = i;
const int &ref = 24;
//类型不匹配赋值失败
int *p = cp; //cp包含底层const定义,而p灭有
int &c = cc; //cc包含底层const定义,而c没有
int &r = 24; //不能用字面值初始化非常量引用
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。字面值就是常量表达式,用常量表达式初始化化的const对象也是常量表达式。
//常量表达式
const int max = 50;
const int limit = max+1;
//不是常量表达式
int size1 = 22;
const int se = size1;
但是在一个复杂系统中很难去欸的那个一个初始值到底是不是常量表达式。C++11标准规定可以使用constexpr
来检查变量的值是否是常量表达式。声明为constexpr
的变量一定是一个常量而且必须用常量表达式初始化。
auto一般会忽略掉顶层const;如果希望推断出的auto是一个顶层const就需要明确指出。设置类型为auto的引用时,初始值的顶层const属性仍然保留。
const int ci = 20;
auto r = ci; //r是一个整数,顶层const特性被忽略了
auto p = &ci; //p是一个整型指针
const auto cr = ci; //cr是cnost int 类型,顶层const
auto &g = ci; //整型常量引用
const auto &j = ci; //常量引用
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
string类型的初始化分为直接初始化和拷贝初始化;
os<),从流is中读取字符串到s(is>>s
)
实际使用中
getline
得到的换行符会被丢弃,所以由此得到的string字符串并不包含换行符
string可以类似数组进行下标访问,其下标类型是string::size_type
类型
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"
迭代器用来模拟类似于下标的访问;常用的有string与vector支持的迭代器
首先来说,需要了解的是迭代器的两个重要的成员(begin和end),需要着重记住的是end成员指向的是尾元素的下一个位置。在C++11的标准中对于迭代器的声明一般使用auto,这样可以避免很多的人为错误。迭代器的运算符操作比较简单这里不再赘述。下面举一个简单使用迭代器的例子
//代码的功能是将字符串s的字母全部变为大写
string s = "hello world";
for(auto it =s.begin();s!=s.end();++it){
*it = toupper(*it)
}
关于迭代器的类型问题:
vectot::iterator
.vectot::const_iterator
类型.前者可以通过迭代器进行读写,后者则只能进行读。其实,在理解上我们可以确实将迭代器当作一个指针来操作,就像上面的示例代码中的对s的特定字符的操作就需要用*
号来进行解引用。注意:但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素,对于这一点有以下几种解释:
1.当插入元素后,end操作返回的迭代器是失效的;
2.插入一个元素后,capacity返回值也会改变
3.进行删除操作时,指向删除点的迭代器全部失效
下面是一个利用迭代器将一段话text转为大写的代码例子
#include
#include
#include
#include
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;
}
}
}
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;
数组类似于vector,通过下标进行访问,但是不同的是它不能进行动态元素添加。而且不同于vector的是,数组不可以进行拷贝和复制,也就是不能通过一个数组来初始化另一个数组。如以下代码就是错误的;
另外在数组的学习过程中对于一些比较复杂的数组声明应该加以着重理解。一般来说按照数组的名字开始从内向外开始顺序阅读。
在数组的学习中应该理解一下字符数组的特殊性,如果用一个字符串来初始化字符数组,则其最后一个元素是一个空字符。
char a[] = {'C','+','+','\0'}
char a1[3] = "HEL"; //错误,没有空间放空字符'\0'
char a2[] = "HEL"; //{'H','E','L','\0'}
提到指针与数组是因为在大多数表达式中,数组类型的对象其实就是一个指向数组首元素的指针。另外,对于下面的数组nums
,有一些概念需要我们把握。首先我们要知道如何根据数组名作为一个指针来取数组的值。
下面第三行代码中的*p
是指向了数组首元素的指针,对其进行加一,是以元素int大小为标准进行内存地址的计算。&nums[0]
以及数组名nums
也是类似的。但是对于&nums
,首先我们前面已经提到数组名本身就是一个指针、&
是一个取地址符,对指针nums
再进行取地址是可行的吗?这样理解的话就有些片面了。直接使用&nums的话,此时的nums就被看作了一个普通的变量,这个取值的结果也就是求出了数组的首地址。注意体会数组首元素的地址和数组的首地址的区别。对于数组首地址加一的话,此时的跳跃值就是整个数组所占的内存大小。所以这也就是为什么下面的&nums+1
的输出是0x277e9ff7f0+20=0x277e9ff804
的缘故。
需要牢记的是,使用数组的时候其实真正使用的是指向数组元素的指针。
int nums[] = {1,2,3,4,5};
int *p=nums;//等价于string *p=&nums[0];
cout<<*p<<" "<<*(p+1)<<endl; //输出one two
cout<<nums<<endl; //输出0x277e9ff7f0
cout<<&nums[0]<<endl; //输出0x277e9ff7f0
cout<<&nums[0] +1 <<endl; //输出0x277e9ff7f4
cout<<nums+1<<endl; //输出0x277e9ff7f4
cout<<&nums+1<<endl; //输出0x277e9ff804
指针其实也是迭代器,提到指针我们就很容易想到迭代器这个类型,其实数组的指针就相当于一个迭代器。迭代器是有首尾元素的,也可以根据这个求出数组的首尾指针,类似的原理就是对于数组的尾指针也是指向最后一个元素的下一个位置(下标不存在的元素)。为了防止我们在自己使用时候出错,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
有一个比较无用的概念:对于内置类型来说,下标的值可以不是无符号类型,而标准库类型(如vector和string)的下标就必须是无符号类型。
int arr[10];
int *p1[10]; //含有十个指针的指针数组
int (*p2)[10]; //指向一个十个数数组的数组指针
具体使用区别后续了解。首先看下面一个使用指针数组打印C++命令行和环境变量的程序
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
int main(int argc, char *argv[], char **env)
{
int i = 0;
printf("================Begin argv====================\n");
for (i = 0; i < argc; i++)
{
printf("%s\n", argv[i]);
}
printf("================End argv====================\n");
printf("\n");
printf("\n");
printf("\n");
printf("================Begin env====================\n");
for (i = 0; env[i] != NULL; i++)
{
printf("%s \n", env[i]);
}
printf("================End env====================\n");
getchar();
return 0;
}
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();
需要注意的是,数组不可以初始化数组,向量也不能初始化数组。
int ia[]={1,2,3,4,5,6,7,8,9,10};
vector<int>ivec (begin(ia),end(ia));
将二维数组理解为数组的数组,高维继续类推就可以了。
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
在实际应用中我们要学会使用C++提供的范围for语句访问多维数组,下面是一个具体实例,注意:要使用范围for语句对多维数组进行操作则必须将除最内层以外的控制变量声明为引用类型,这是因为外层其实此时就是一个数组,如果相应的控制变量不是引用类型就会导致数据类型转换不合法。
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;
}
C++标准中只规定了少数的二元运算符的求值顺序(逻辑与&&、逻辑或||、逗号运算符和条件运算符?:都是从左到右,先计算左侧),对于其他大多数运算符来说并没有明确规定,这在某种程度上加快了代码生成的效率,但是有些时候会引发潜在的危险。
所以在同一个表达式中如果有两个相同的对象,那么就会引发未定义的行为。如代码int i=0;cout<的输出结果可能是
1 1
,也可能是0 1
等等.所以我们在复合表达式的书写中可以注意:
1.拿不准的时候可以使用括号来进行强制确定逻辑关系;
2.如果修改了表达式中一个对象的值,那么最好不要在同一表达式中使用它
但是:(如果另一个包含该对象的表达式本身就是另外一个表达式的子表达式则例外,如*++iter就没有问题,因为此时的++iter就是*++iter的子表达式)。
int factorial(int x){
if (x>1){
return factorial(x-1)*factorial(x);
}
else return 1;
}
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**
计算机中算术类型的存储是补码形式;
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
sizeof
运算符,取地址符&
以及typeid
等)底层const
int i=99;
const int j=100;
const int &r = i;
const int *p = &i;
//int &tr = r;
//int *re = &j;
显式转换也叫强制类型转换,这种方法是很危险的。强制类型转换的一般语法结构是cast-name
其中cast-name
是转换类型的类别,主要有以下四种
左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。
参考这篇文章
异常是指程序的某部分检测到它无法处理的问题时,就会用到异常处理。专门的异常会有专门的处理。C++的异常处理机制是通过三个部分完成的,分别是throw表达式、try语句块、以及异常处理代码。
一般来说异常处理使用的是try语句块:
try{
programe-statements
throw;
}catch(exception-declaration){
handler-statements
}
programe-statements
是加入监测的程序代码;exceptiton-declaration
是异常声明,一般是指明是何种异常;handler-statements
是异常类型匹配成功后进行处理的语句。一般来说一个try语句后可以跟多个catch语句,也就是说可以对一段程序的异常进行多种类型的检测。throw
**try
语句是可以嵌套的。在嵌套的try
语句中必然有着嵌套的多个catch
语句。try
语句嵌套的情况,当异常发生时,首先要搜索相匹配的catch
语句进行异常处理,此时如果未找到相应的catch
处理语句,那么就要在调用这个try
语句块的函数中寻找最近的catch
语句块,如果最终没有找到catch
语句,那么程序会执行中止函数的操作,其实也就是一个名为terminate
的标准库函数。总的来说就是沿着程序执行路径逐层回退寻找catch
语句。举个例子就是,一个没有任何
try
语句块定义的语句发生了异常,它没有相应的catch
语句块,所以就会执行terminate
函数并终止当前程序的运行。
异常类一般会提供异常出错信息
表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;
}
return 0;
,这是因为如果控制到了main的结尾处而没有此语句,编译器会隐式执行该语句。一般来说,main
函数返回0表示程序成功退出;返回值是其他非零值的话具体含义依据机器决定。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;
}
引用传递和值传递辨析:
void SWAP(int p1,int p2);
void SWAP(int &p1,int &p2)
void SWAP(int *p1,int *p2)
在使用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 *
这样的话不论实参是常量值还是非常量值都是可行的,出于设计的考虑这是比较科学的。
前面就介绍过,在数组实际使用中,其实就相当于将数组名看作一个指向首元素的指针,所以对于数组作为函数参数,就只是首元素指针的值传递,因为其本身就是一个指针。数组做形参有三种形式,分别是:
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;
}
主要是新标准中用于不确定函数参数的声明;
函数一般都是要执行return用作返回值的,但是void
类型函数可以隐式执行return;
。return可以执行返回类型检查;
执行return后,返回值根据返回类型到达函数调用点(如果不是返回引用则需要拷贝回去)。
就是说函数返回值返回引用时,得到的是左值,可以像使用其它左值一样返回引用函数的调用;
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;
}
区分数组指针和普通的指向数组首元素的指针,前者指向的是整个数组空间,后者指向的是一个元素。这个概念比较繁琐难理解,需要后面再继续了解。
函数声明形式: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);
定义:除了形参不同,其他的都相同(函数名)。编译器会根据形参类型在调用时确定调用的是哪一个。注意其中形参不同的定义。实际中是否使用函数重载取决于重载后的函数调用是否更方便,如果不能则不建议进行函数重载。
形参不同:分别在数量和类型上界定,可以是数量不一样,也可以是数量相同的情况下类型不一样,或者可以是数量和类型均不一样。
C++规定函数重载必须在形参数量或者形参类型上界定
//形参类型项相同不是重载
int get();
double get();
//形参类型不同,是重载
int *reset(int *);
double *reset(double *);
思考:形参顺序不一样算重载吗???
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){
}
具体执行时就是编译器会根据实参是否是常量来决定调用第二个函数还是第一个函数。因为在实际使用中const不能转换为其他类型,而非常量可以转换为const,所以说可以将非常量实参传递给常量形参,反过来则不行。
前面(4.3.3)已经在类型转换里见过static_cast
,const_cast
等等,这里所说的const_cast
形参其实就是对形参进行强制类型转换。
如果const没有修饰*指针或&引用,那么其类型其实并没有变化
所以说有时候加了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);
}
不同的作用域中无法重载函数名
编译器处理一个名称时会首先在距离最近的作用域寻找,就是说如果有局部作用域一般会首先在局部作用域寻找。
1)默认实参声明方式
二义性
)。在函数调用时,实参按照其位置解析,默认实参负责填补函数调用时缺少的尾部实参。在具体设计时要注意尽量让不怎么使用默认值的形参出现在前面。下面就是一个默认实参的例子。//声明
void read(int j,string name = "He", float price = 100);
//调用
read(m);
2) 局部变量不能作为实参
在我们声明默认实参的时候有时参数可以是某个方法,也可以是某个变量或者某个表达式(前提是结果类型相同),这就使得程序编写的时候有了很大的灵活性,但是注意默认实参不能为局部变量。看下面一个例子
int ht();
string sd;
void read(int j=ht(),string name = sd, float price =ht()+10);
void f1(){
sd = "op";
read(); //默认实参被改编为“op”
string sd = "oi";//局部变量不能作为默认实参
//所以虽然隐藏了外层的sd,但是没有改变默认参数
read();
}
1)内联函数的作用和声明
一般来说,涉及到函数的调用涉及到一系列的工作,比如调用前寄存器的保存、返回函数后寄存器的恢复、实参的拷贝等等。对于一些频繁调用而且又比较小的函数(调用开销大于执行开销)而言,这就造成准备与恢复开销远大于其执行的开销,这样C++就提出了内联函数。
调用
流程来执行的程序就通过编译器地预处理直接“copy”到了调用函数的地方。从而减少一些不必要的调用开销。inline [返回类型] [函数名称] (参数列表) {函数体}
,下面就是一个例子inline string make_plural(size_t ctr,const string &word,const string &end = "s"){
return (ctr>1)? word+end:word;
}
int main(){
cout<<make_plural(2,"success");
}
这个函数在编译过程中会展开为cout<< (2>1)? "success"+"s" : "success";
2)constexpr函数
constexpr
在前面第二章已经介绍过,在函数声明中它的作用是声明一种能用于常量表达式的函数。constexpr
返回类型的函数在执行初始化任务时,编译器将该函数的调用替换成其结果值。constexpr
返回类型的函数有以下几个规定:
return
语句constexpr int nes() {return 32;}
注意constexpr
函数不一定就一定返回一个常量表达式,也有可能因为类型错误发生编译器错误。
注:
编译器:就是把源代码翻译成目标代码的工具,目标代码可以是机器码,也可以是其他代码
预处理器:就是在代码交给编译器处理前,预先进行一些处理,比如包含头文件,宏展开等等
1)assert(只在程序调试时使用)
assert(expr);
经常用来检查不能发生的情况expr
求值,若为真则什么也不做继续执行程序,若为0则assert
输出信息并终止程序的执行。assert()
定义于cassert
头文件中。 #ifndef NDEBUG
cout<<"使用DEBUG来进行调试"<<endl;
#endif
二者关系看下面的源码
#ifdef NDEBUG
/*
* If not debugging, assert does nothing.
*/
#define assert(x) ((void)0)
#else /* debugging enabled */
/*
* CRTDLL nicely supplies a function which does the actual output and
* call to abort.
*/
_CRTIMP void __cdecl __MINGW_NOTHROW _assert (const char*, const char*, int) __MINGW_ATTRIB_NORETURN;
/*
* Definition of the assert macro.
*/
#define assert(e) ((e) ? (void)0 : _assert(#e, __FILE__, __LINE__))
#endif /* NDEBUG */
一些思考解答
首先为什么有了assert还要NDEBUG?这是因为在开发调试阶段,你使用assert进行调试,而且可能代码中很多地方出现了assert 。但当发布正式版时,需要把里面的调试信息去除,这时候如果一个一个删除assert(s),有点麻烦。我们需要一个类似"掩码"一样的东西,屏蔽代码中所有的assert语句。
NDEBUG就是这个东西…当定义NDEBUG后再assert,assert被定义为空语句,即屏蔽; 当未定义NDEBUG,之后的每个assert都起到本应该的断言作用.
assert在cassert头文件中定义,当预处理器对头文件进行展开后,你的’#define NDEBUG’ 其实是出现在assert定义之后的,此时的assert已经被定义为’你本意想要的断言作用’.
就是在函数调用的时候决定调用重载函数中的哪一个的具体过程。比如当几个重载函数形参数量相等而且参数类型可以转换,那么就不是很容易区分调用哪个了。
函数匹配的一般过程为:
其中候选函数就是所有的同名称重载函数,再次基础上我们会根据实际调用的实参数量和可匹配的类型确定出可行函数。再确定了可行函数之后就要再可行函数中寻找出最匹配的函数,主要遵循的原则有:
看书中给出的一个例子,我们看到调用
f(42,2.56)
时发生二义性错误,这是因为,对于第一个参数而言,因为参数本身是int
类型所以会更倾向于调用候选函数中的function 3
,而以同样的方式考量第二个参数会发现它更倾向于调用function 4,编译器会因为二义性而拒绝这个请求。
具体示例代码如下所示:
void f(){
cout<<"function 1"<<endl;
}
void f(int a){
cout<<"function 2"<<endl;
}
void f(int a,int b){
cout<<"function 3"<<endl;
}
void f(double a,double b=3.14){
cout<<"function 4"<<endl;
}
int main(){
vector<int> iv{1,23,4,5,6,7,4};
// f(42,2.56); 二义性错误
// f(2.56,42); 二义性错误
f(42); //function 2
f(42,0); //function 3
f(2.56,3.14); //function 4
}
我们可以再回到上面的二义性例子,其实如果函数3和函数4只有一个的话,那么就不会发生二义性错误,它们还是会进行类型转换来进行函数调用。但是这种设计是不好的,尽量要避免。
函数的参数匹配从高到低分为以下几个等级(注意复习类型转换相关的知识)
区分以下两个函数是否构成重载
int cla(char *p);
int cla(char *const p);
顾名思义,函数指针就是指向函数的指针。注意函数指针的声明和绑定要确保可以匹配。看下面一个例子。注意函数指针的括号不要丢掉。
void print(vector<int> &ivec,unsigned int index){
vector<int>::size_type si = ivec.size();
if(!ivec.empty() && index < ivec.size()){
cout<<ivec[index]<<" ";
print(ivec,index+1);
}
}
int main(){
vector<int> iv{1,23,4,5,6,7,4};
void (*p1)(vector<int> &ivec,unsigned int index);
p1 = print; //等价于p1 = &print;
//下面三种调用等价
p1(iv,2);
(*p1)(iv,2);
print(iv,2);
}
从字面意思理解函数指针,就是一个指向函数的指针。记录函数指针之前首先需要明确函数指针的两个用途:作为调用函数的指针、做函数的参数。下面就是一个简单的函数指针的声明
typedef float(*pf)(float ,float );
在重载函数中使用函数指针可以精确的界定使用哪个函数。因为在声明函数指针时已经预先对其类型进行了声明。编译器通过指针类型决定选用哪个重载函数中的哪个函数,函数指针要求指针类型必须与重载函数中的某一个精确匹配(类型相同,顶层const的忽略,数组与指针的转换)。如对于6.6.1中的代码如果在main
函数加入如下语句,此时的调用就是会调用两个int
类型的。
void (*p2)(int a,int b) = f;
p2(2.56,3.14);
C++中虽然不能定义函数类型的形参,但是却可以用函数指针作为形参。虽然直接用函数声明和函数名也可以作为形参,但是都会类似数组一样当作指针处理。
下面是一个函数指针作为参数的例子,也就是在一系列参数类型相同的函数之间使用函数指针可以使得函数调用更加紧凑。
float f(float(*pfun)(float,float),float a,float b) { return pfun(a,b); } // 四则运算函数
float ad(float a,float b){ return a+b;} // 将用作实际参数的 加法 函数定义
float su(float a,float b){ return a-b;} // 将用作实际参数的 减法 函数定义
int main(){
float a=3,b=9;
cout<<"add is "<<f(ad,a,b);
cout<<"sub is "<<f(su,a,b);
}
下面是一个利用函数指针实现计算器的例子
float ad(float a,float b){ return a+b;}
float su(float a,float b){ return a-b;}
float mi(float a,float b){ return a*b;}
float di(float a,float b){ return a/b;}
void compute(float a,float b,float (*p)(float,float)){
cout<<p(a,b)<<endl;
}
int main(){
float a=3,b=9;
decltype(ad) *p1 = ad,*p2 = su,*p3 = mi,*p4 = di;
vector<decltype(ad) *>vF = {p1,p2,p3,p4};
for(auto p:vF){
compute(a,b,p);
}
}
类似于返回指向数组的指针;