C++Primer第五版 基础部分阅读笔记

C++Primer 第五版中文版阅读笔记2~6

  • 第二章 基本内置类型
    • 2.1复合类型
    • 2.2const限定符
      • 2.2.1常量与引用
      • 2.2.2常量与指针
      • 2.2.3顶层const和底层const
    • 2.3 constexpr和常量表达式
    • 2.4 auto类型说明符
  • 第三章 string、vector和数组
    • 3.1 string类型
      • 3.1.1 初始化
      • 3.1.2 string上的操作
      • 3.1.3string下标访问
    • 3.2.vector类型
      • 3.2.1 初始化
      • 3.2.2
    • 3.3.迭代器
      • 3.3.1如何使用迭代器
      • 3.3.2迭代器的运算
    • 3.4 数组
      • 3.4.1指针和数组
      • 3.4.2指针数组和数组指针
      • 3.4.3 C风格字符串
      • 3.4.4 数组可以用来初始化vector
      • 3.4.5 多维数组
  • 第四章 表达式
    • 4.1求值顺序
    • 4.2sizeof运算符
    • 4.3类型转换
      • 4.3.1算术转换
      • 4.3.2其他隐式转换
      • 4.3.3显式转换
    • 4.4 左值和右值
  • 第五章 语句
    • 5.1 异常处理
      • 5.1.1 一般概念
      • 5.1.2异常处理中函数的退出
      • 5.1.3 标准异常类
  • 第六章 函数
    • 6.1 函数基础
      • 6.1.1 易混淆概念
      • 6.1.2main函数深究
    • 6.2 *参数传递*
      • 6.2.1 引用传递的优点
      • 6.2.2 常量引用形参
      • 6.2.3 尽量使用常量引用
      • 6.2.4 数组形参
      • 6.2.5 可变形参
    • 6.3 retuen语句与函数返回值
      • 6.3.1 值如何被返回?
      • 6.3.2 引用返回左值
      • 6.3.3 返回数组指针/引用
    • 6.4 函数重载
      • 6.4.1 const形参和重载
      • 6.4.2 const_cast形参和重载
      • 6.4.2 作用域和重载
    • 6.5 特殊语言特性
      • 6.5.1 默认实参
      • 6.5.2 内联函数和constexpr
      • 6.5.3 调试帮助
    • 6.6 函数匹配
      • 6.6.1 函数匹配一般定义
      • 6.6.2 实参的类型转换
    • 6.7 函数指针
      • 6.7.1 函数指针初探
      • 6.7.2 重载函数的函数指针
      • 6.7.3 函数指针作为形参
      • 6.7.3 返回指向函数的指针

第二章 基本内置类型

2.1复合类型

2.2const限定符

const表示一个量的值不能被修改,例如进行如下定义const int buff=512;留意以下的情况

	int i =1;
    const int cnt = i;

在VSCode中认为cnt不是一个完整的常量,这是因为认为尽管cnt是一个整型常量,但是它的常量特征只在修改其值时发生作用。下面是一个实例
C++Primer第五版 基础部分阅读笔记_第1张图片

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;	//指向常量的常量指针

从右向左读,读的时候看const距离谁最近;

2.2.3顶层const和底层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;	//不能用字面值初始化非常量引用

C++Primer第五版 基础部分阅读笔记_第2张图片

2.3 constexpr和常量表达式

常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。字面值就是常量表达式,用常量表达式初始化化的const对象也是常量表达式。

 //常量表达式
const int max  = 50;
const int limit = max+1;
 //不是常量表达式
int size1 = 22;
const int se = size1;

但是在一个复杂系统中很难去欸的那个一个初始值到底是不是常量表达式。C++11标准规定可以使用constexpr来检查变量的值是否是常量表达式。声明为constexpr的变量一定是一个常量而且必须用常量表达式初始化。

2.4 auto类型说明符

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;	//常量引用

第三章 string、vector和数组

你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。

3.1 string类型

3.1.1 初始化

string类型的初始化分为直接初始化和拷贝初始化;

3.1.2 string上的操作

  • 可以留意string与流的操作,如将s写入到流os中(os<),从流is中读取字符串到s(is>>s
  • string在读取是会自动忽略头部的空白,并在遇到下一个空白时停止,为了弥补这个可以应用到getline读取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)
}

关于迭代器的类型问题

  1. 虽然在一般使用中我们可以使用auto来进行迭代器的类型说明,但是还是需要了解到一些具体概念,比如对于int数据类型组成的vector而言,它的迭代器类型就是vectot::iterator.
  2. 对于迭代器而言也有着类似于常量指针的常量迭代器,如上述对应的就是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;
    }
    }
}

3.3.2迭代器的运算

  1. 迭代器的运算一般指迭代器的前进后退操作,这一般是通过重载的运算符+和-等实现的,使用比较简单。类似于数值的运算。
  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指针和数组

提到指针与数组是因为在大多数表达式中,数组类型的对象其实就是一个指向数组首元素的指针。另外,对于下面的数组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)的下标就必须是无符号类型。

  • 复杂的数组声明比较难懂从数组的名字开始由内到外顺序阅读(参见原书103页)

3.4.2指针数组和数组指针

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;
}

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 多维数组

将二维数组理解为数组的数组,高维继续类推就可以了。

  1. 多维数组的下标引用
    只要是数组就可以采用下标进行访问,但是如果只用一个下标运算符的话就会出现不同的结果,正如我们前面提到的,对数组元素的访问其实都是通过指针进行的,所以使用下标就有以下两种情况:
    -下标运算符数量和数组维度一样,访问的就是数组元素
    -下标运算符少于数组维度,访问的就是指定索引处的一个内层数组。
    下面是一个代码示例
    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;
        }
    }
  1. 指针和多维数组
    多维数组与指针的关系类似于一维数组与指针的关系,通过类推我们可以得出,指向多维数组的指针实际上是一个指向数组的指针。
	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<的输出结果可能是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;
 }

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(expression)其中cast-name是转换类型的类别,主要有以下四种

  • static_cast:只要不包含底层const都可以通过这个类型进行类型转换。
  • const_cast:可以用来改变运算对象的底层const资格;也就是说这个形式的强制类型转换可以改变表达式的常量属性,可以使得常量可以被修改。但是const_cast不能修改变量的类型。
    在这里插入图片描述
  • reinterpret_cast:风险很大,尽量避免使用
  • dynamic_cast:支持运行时类型识别

4.4 左值和右值

左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。

参考这篇文章

第五章 语句

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语句块定义的语句发生了异常,它没有相应的catch语句块,所以就会执行terminate函数并终止当前程序的运行。

  • 我们需要再理解一个概念:那就是函数在寻找异常处理代码的过程中退出
    不难想到,一旦发生异常,那么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 易混淆概念

  1. 形参和实参必须相匹配(类型可以进行最终匹配、数量不能多)
  2. 函数三要素:返回类型、形参类型和函数名

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 引用传递的优点

  1. 不必拷贝实参的值,节省空间;
  2. 可以直接操作实参对象;
  3. 可以帮助实现返回多个值
    所以分析为什么使用引用传递或者不使用引用传递应该从以上三方面进行分别考虑。

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[]){}

一旦使用指针就必须要注意边界情况,在数组元素的边界确定时,主要使用三种方法来管理

  1. 遇空指针停止
  2. 首尾元素指针beginend(尾元素的下一个位置)进行控制
  3. 确定范围值做循环控制

下面是一个设计指针形参的代码

//交换两个指针本身
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 函数重载

定义:除了形参不同,其他的都相同(函数名)。编译器会根据形参类型在调用时确定调用的是哪一个。注意其中形参不同的定义。实际中是否使用函数重载取决于重载后的函数调用是否更方便,如果不能则不建议进行函数重载。

形参不同:分别在数量和类型上界定,可以是数量不一样,也可以是数量相同的情况下类型不一样,或者可以是数量和类型均不一样。
C++规定函数重载必须在形参数量或者形参类型上界定

//形参类型项相同不是重载
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){ 
}

具体执行时就是编译器会根据实参是否是常量来决定调用第二个函数还是第一个函数。因为在实际使用中const不能转换为其他类型,而非常量可以转换为const,所以说可以将非常量实参传递给常量形参,反过来则不行。

6.4.2 const_cast形参和重载

前面(4.3.3)已经在类型转换里见过static_castconst_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);
	}

6.4.2 作用域和重载

不同的作用域中无法重载函数名
编译器处理一个名称时会首先在距离最近的作用域寻找,就是说如果有局部作用域一般会首先在局部作用域寻找。

6.5 特殊语言特性

6.5.1 默认实参

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();
}

6.5.2 内联函数和constexpr

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函数不一定就一定返回一个常量表达式,也有可能因为类型错误发生编译器错误。

6.5.3 调试帮助

注:

编译器:就是把源代码翻译成目标代码的工具,目标代码可以是机器码,也可以是其他代码
预处理器:就是在代码交给编译器处理前,预先进行一些处理,比如包含头文件,宏展开等等

1)assert(只在程序调试时使用)

  • 用法:assert(expr);经常用来检查不能发生的情况
  • 说明:执行时首先对expr求值,若为真则什么也不做继续执行程序,若为0则assert输出信息并终止程序的执行。assert()定义于cassert头文件中。
  • assert是由预处理器处理的,并非由编译器处理
    2)NDEBUG
    NDEBUG宏是C++标准中定义的宏,专门用来控制assert()的行为。如果定义了这个宏,则assert不会起作用。因为默认状态下没有定义NDEBUG所以才会执行assert检查。使用方法:
    #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已经被定义为’你本意想要的断言作用’.

6.6 函数匹配

6.6.1 函数匹配一般定义

就是在函数调用的时候决定调用重载函数中的哪一个的具体过程。比如当几个重载函数形参数量相等而且参数类型可以转换,那么就不是很容易区分调用哪个了。
函数匹配的一般过程为:

有可行
无可行
确定候选函数
确定可行函数
寻找最佳匹配
编译器报告无匹配错误
编译器报告二义性错误

其中候选函数就是所有的同名称重载函数,再次基础上我们会根据实际调用的实参数量和可匹配的类型确定出可行函数。再确定了可行函数之后就要再可行函数中寻找出最匹配的函数,主要遵循的原则有:

  • 分别从每个参数进行考虑
  • 对于每个参数来说要尽量减少强制类型转换

看书中给出的一个例子,我们看到调用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
}

6.6.2 实参的类型转换

我们可以再回到上面的二义性例子,其实如果函数3和函数4只有一个的话,那么就不会发生二义性错误,它们还是会进行类型转换来进行函数调用。但是这种设计是不好的,尽量要避免。
函数的参数匹配从高到低分为以下几个等级(注意复习类型转换相关的知识)

Created with Raphaël 2.3.0 精确匹配(形参实参类型相同、数组与指针的转换、忽略顶层const的转换) 通过const转换实现的匹配(一般是非常量转换为常量) 通过类型提升实现的匹配 通过算术类型转换或指针转换实现的匹配 通过类类型转换实现的匹配

区分以下两个函数是否构成重载

int cla(char *p);
int cla(char *const p);

6.7 函数指针

6.7.1 函数指针初探

顾名思义,函数指针就是指向函数的指针。注意函数指针的声明和绑定要确保可以匹配。看下面一个例子。注意函数指针的括号不要丢掉。

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);
}

6.7.2 重载函数的函数指针

从字面意思理解函数指针,就是一个指向函数的指针。记录函数指针之前首先需要明确函数指针的两个用途:作为调用函数的指针、做函数的参数。下面就是一个简单的函数指针的声明

typedef float(*pf)(float ,float );

在重载函数中使用函数指针可以精确的界定使用哪个函数。因为在声明函数指针时已经预先对其类型进行了声明。编译器通过指针类型决定选用哪个重载函数中的哪个函数,函数指针要求指针类型必须与重载函数中的某一个精确匹配(类型相同,顶层const的忽略,数组与指针的转换)。如对于6.6.1中的代码如果在main函数加入如下语句,此时的调用就是会调用两个int类型的。

void (*p2)(int a,int b) = f;
    p2(2.56,3.14);

6.7.3 函数指针作为形参

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);
    }
}

6.7.3 返回指向函数的指针

类似于返回指向数组的指针;

你可能感兴趣的:(C++,c++)