C++primer阅读笔记
C++primer阅读笔记
第一部分 C++基础
第二章 变量和基本类型
2.1 基本内置类型
-
2.1.1 算术类型
-
2.1.2 类型转换
-
2.1.3 字面值常量
字符和字符串字面值
前缀 含义 类型 u Unicode16字符 char16_t U Unicode32字符 char32_t L 宽字符 wchar_t u8 UTF-8(仅用于字符串字面常量) char 整型字面值
后缀 最小匹配类型 u/U unsigned l/L long ll/LL long long 浮点型字面值
后缀 类型 f/F float l/L long double
2.2 变量
- 2.2.1 变量定义
-
c++11标准:使用花括号来初始化变量-列表初始化
-
定义在函数体内部的内置类型变量将不被初始化
定义在函数体之外的变量即全局变量放在全局区存储,并初始化成0。定义在函数体内的局部变量在栈中分配空间时如果初始化成0需要额外的时间和空间都有成本
-
类的对象如果没有显示地初始化,则其值由类确定
-
- 2.2.2 变量声明和定义的关系
- C++语言支持分离式编译机制,该机制语序将程序分割为若干个文件,每个文件可被独立编译
- 如果想声明一个变量而非定义它,就在变量名前添加关键字
extern
,而且不要显式地初始化变量 - 变量能且只能被定义一次,但是可以被多次声明
- 要想在多个文件中使用同一个变量,则要将声明和定义分离,并且只在一个文件里定义,在其他文件里声明
- C++是一种静态类型语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查。
- 2.2.3 标识符
- 2.2.4 名字的作用域
2.3 复合类型
- 2.3.1 引用
- 2.3.2 指针
- C++11标准:使用字面值
nullptr
来初始化指针 - 过去使用
NULL
的预处理变量来给指针复制,这个变量在头文件cstdlib
中定义,它的值就是0 - 把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行
- 不能直接操作
void*
指针所指的对象
- C++11标准:使用字面值
- 2.3.3 理解复合类型的声明
- 面对一条比较复杂的指针或引用的声明语句时,从右往左阅读有助于弄清楚它的真实含义
2.4 const限定符
-
默认状态下,const对象仅在文件内有效
-
如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字
-
2.4.1 const的引用
- 初始化常量引用可以用变量或者表达式
-
2.4.2 指针和const
- 允许一个常量指针指向一个非常量对象
- 所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变
-
2.4.3 顶层const
-
顶层const:表示指针本身是一个常量
-
底层const:表示指针所指向的对象是一个常量
-
当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之不行
-
更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用;底层 const 则与指针和引用等复合类型有关,比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const 或者二者兼备。
int i = 0; int *const p1 = &i; // 不能改变 p1 的值,这是一个顶层 const int ci = 42; // 不能改变 ci 的值,这是一个顶层 const int *p2 = &ci; // 允许改变 p2 的值,这是一个底层 const int *const p3 = p2;// 靠右的const是顶层const,靠左的是底层const const int &r = ci; // 所有的引用本身都是顶层 const //因为引用一旦初始化就不能再改为其他对象的引用 //这里用于声明引用的 const 都是底层 const
-
-
2.4.4 constexpr和常量表达式
-
尽管sz本身是一个常量,但他的具体值直到运行时才能获取到,所以不是常量表达式
const int sz = get_size();
-
constexpr变量
-
C++11标准:允许将变量声明为
constexpr
类型以便由编译器来验证变量的值是否是一个常量表达式 -
自定义类Sales_item、IO库、string类型不属于字面值类型,也就不能被定义成
constexpr
-
尽管指针和引用都能定义成
constexpr
,但他们的初始值都受到严格的限制。一个constexpr
指针的初始值必须是nullptr
或者0,或者是存储于某个固定地址中的对象。函数体内定义的变量一般来说并非存放在固定地址中,因此cosntexpr指针不能指向这样的变量。
定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr指针。
特殊:允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这种的变量上,cosntexpr指针也能指向这样的变量。
-
尽管
constexpr
能验证变量的值是否是一个常量表达式,但是constexpr
指针还是能指向变量(但必须是常量)
-
-
2.5 处理类型
-
2.5.1 类型别名
类型别名是一个名字,它是某种类型的同义词
有两种方法可以定义类型别名
-
传统的方法是使用关键字typedef
typedef double wages;
-
新标准规定了一种新的方法,使用别名声明来定义类型的别名
using SI = Sales_item;
-
如果某个类型别名指代的是复合类型或常量,那么它用到声明语句里就会产生意想不到的后果。
下面的声明语句用到了类型
pstring
,它实际上类型char*
的别名typedef char *pstring;
-
-
2.5.2 auto类型说明符(C++11标准引入)
-
auto定义的变量必须有初始值
-
使用auto也能在一条语句中声明多个变量
因为一条声明语句只能有一个基本数据类型,所以该语句中所有的变量的初始基本数据类型都必须一样:
auto i = 0, *p = &i; // 正确 auto sz = 0, pi = 3.14; // 错误
-
auto一般会忽略掉顶层const,同时底层const则会保留下来:
const int ci = i, &cr = ci; auto b = ci; //b是一个整数(ci的顶层特性被忽略掉了) auto c = cr; //c是一个整数(cr是ci的别名,ci本身是一个顶层const) auto d = &i; //d是一个整型指针(整数的地址就是指向整数的指针) auto e = &ci; //e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci;//ci的推演类型是int,f是const int
设置一个类型为auto的引用时,初始值中的顶层属性依然保留:
auto &g = ci;//g是一个整型常量引用,绑定到ci auto &h = 42;//错误:不能为非常量引用绑定字面值 const auto &j = 42;//正确:可以为常量引用绑定字面值
要在一条语句中定义多个变量,切记,符号
&
和*
只从属某个声明符,而非基本数据类型的一部分,因此初始值必须是同一类型:auto k = ci, &l = i;//k是整型,l是整型引用 auto &m = ci, *p = &ci;//m是对整型常量的引用,p是指向整型常量的指针 auto &n = i,*p2 = &ci;//错误:i的数据类型是int而&ci的类型是const int
-
-
2.5.3 decltype类说明符(C++11标椎引入)
-
C++11标椎引入了第二种类型说明符
decltype
,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:decltype(f()) sum = x;//sum的类型就是函数f的返回类型
-
decltype
处理顶层const和引用的方式与auto
有些许不同。如果decltype
使用的表达式是一个变量,则decltype
返回该变量的类型(包括顶层const和引用在内):const int ci = 0, &cj = ci; decltype(ci) x = 0;//x的类型是const int decltype(cj) y = x;//y的类型是const int& decltype(cj) z;//错误:z是一个引用,必须初始化
引用从来都作为其所指对象的同义词出现,只有用decltype处是一个例外
-
如果
decltype
使用的表达式不是一个变量,则decltype
返回表达式结果对应的类型。有些表达式将向
decltype
返回一个引用类型:// decltype的结果可以是引用类型 int i = 42, *p = &i, &r = i; decltype (r + 0) b; //正确:加法的结果是int,因此b是一个(未初始化的)int decltype (*p) c; //错误:c是int&,必须初始化
因为
r
是一个引用,因此decltype(r)
的结果是引用类型。如果想让结果类型是r所指的类型,可以把r
作为表达式的一部分,如r+0
,显然这个表达式的结果将是一个具体值而非一个引用。如果表达式的内容是解引用操作,则
decltype
将得到引用类型。正如我们所熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)
的结果类型就是int&
,而非int
。 -
如果给变量加上了一层或多层括号,编译器就好把他当成是一个表达式:
// decltype的表达式如果是加上了括号的变量,结果将是引用 decltype((i)) d;// 错误:d是int&,必须初始化 decltype(i) e;//正确:e是一个(未初始化的)int
-
decltype((variable))
(注意是双层括号)的结果永远是引用,而decltype(variable)
结果只有当variable
本身就是一个引用时才是引用。
-
2.6 自定义数据结构
-
2.6.1 定义Sales_data类型
-
一般来说,最好不要把对象的定义放在一起。
-
类体定义类的成员,我们的类只有数据成员(data member)。
-
C++11标准规定,可以为数据成员提供一个类内初始值(in-class initializer)。
可以放在花括号里或者等号右边,记住不能使用圆括号
-
-
2.6.2 使用Sales_data类
-
2.6.3 编写自己的头文件
- 预处理概述
- 确保头文件多次包含仍能安全工作的常用技术是预处理(preprocessor)
- C++程序还会用到的一项预处理功能是头文件保护符(header guard)
-
预处理变量有两种状态:已定义和未定义。
#define
指令把一个名字设定为预处理变量 -
另外两个指令则分别检查某个指定的预处理变量是否己经定义:
#ifdef
当且仅当变量已定义时为真#ifndef
当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif
指令为止。
-
- 预处理概述
第三章 字符串、向量和数组
3.1 命名空间的using声明
-
头文件不应包含using声明
位于头文件的代码一般来说不应该使用using声明。这是因为头文件的内容会拷贝到所有引用它的文件中去,如果头文件里有某个using声明,那么每个使用了该头文件的文件就都会有这个声明。对丁某些程序来说,由于不经意间包含了一些名字,反而可能产生始料未及的名字冲突。
3.2 标准库类型string
-
3.2.1 定义和初始化string对象
- 拷贝初始化字符串字面值是不包含
\0
- 如果使用等号
=
初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。与之相反,如果不使用等号,则执行的是直接初始化(direct initialization)。
- 拷贝初始化字符串字面值是不包含
-
3.2.2 string对象上的操作
-
string的操作
os<<s 将s写到输出流os当中,返回os is>>s 从is中读取字符串赋给s,字符串以空白分隔,返回is getline(is, s) 从is中读取一行赋给s,返回is -
string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止。
-
读取未知数量的string对象
while (cin >> word)
该条件负责在读取时检测流的情况,如果流有效,也就是说没遇到文件结束标记或非法输入,那么执行while语句内部的操作。此时,循环体将输出刚刚从标准输入读取的内容。重复若干次之后,一旦遇到文件结束标记或非法输入循环也就结束了。
-
使用
getline
读取一整行虽然读取换行符但是string中并不存换行符
while (getline(cin, line)) //每次读取一行,直至到达文件末尾
触发getline函数返回的那个换行符实际上被丢弃掉了,得到的string对象中并不包含该换行符
-
size函数返回的是个
string::size_type
类型的值,下面就对这种新的类型稍作解释。string::size_type
是一个无符号类型的值如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果。例如,假设n是一个具有负值的
int
,则表达式s.size()<n
的判断结果几乎肯定是true
。这是因为负值n
会自动地转换成一个比较大的无符号值。如果一条表达式中已经有了
size()
函数就不要再使用int
了,这样可以避免混用int
和unsigned
可能带来的问题; -
不能把字面值直接相加
string s7 = "hello" + ", " + s2;//错误
因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象。切记,字符串字面值与string是不同的类型。
-
-
3.2.3 处理string对象中的字符
-
在cctype头文件中定义了一组标准库函数处理这部分工作
cctype头文件中的函数
isalnum(c) 当c是字母或数字时为真 isalpha(c) 当c是字母时为真 iscntrl(c) 当C是控制字符时为真 isdigit(c) 当C是数字时为真 isgraph(c) 当C不是空格但可打印时为真 islower(c) 当C是小写字母时为真 isprint(c) 当C是可打印字符时为真(即C是空格或C具有可视形式) ispunct(c) 当C是标点符号时为真(即C不是控制字符、数字、字母、可打印空白中的一种) isspace(c) 当C是空白时为真(即C是空格、横向制表符、纵向制表符、回车符、换行符、进纸符中的一种) isupper(c) 当c是大写字母时为真 isxdigit(c) 当C是十六进制数字时为真 tolower(c) 如果C是大写字母,输出对应的小写字母;否则原样输出C toupper(c) 如果C是小写字母,输出对应的大写字母;否则原样输出C cctype头文件和ctype.h头文件的内容是一样的,只不过从命名规范上来讲更符合C++语言的要求。特别的,在名为cname的头文件中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然。
-
C++11标准提供了范围for语句
for (declaration : expression) statement
如果想要改变string对象中字符的值,必须把循环变量定义成引用类型
for (auto &c : s)
-
3.3 标准库类型vector
-
3.3.1 定义和初始化vector对象
-
因为vector”容纳着“其他对象,所以它也常被称作容器(container)。
-
编译器根据模板创建类或函数的过程称为实例化(instantiation)
-
vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如
vector<int>
-
在早期版本的C++标准中如果vector的元素还是vector(或者其他模板类型),则其定义的形式与现在的C++11新标准略有不同。过去,必须在外层vector对象的右尖括号和其元素类型之间添加 空格,如应该写成
vector<vector<int> >
而非vector<vector<int>>
-
C++11标准还提供了另外一种为vector对象的元素赋初值的方法,即列表初始化
C++语言提供了几种不同的初始化方式。在大多数情况下这些初始化方式可以相互等价地使用,不过也并非一直如此。目前已经介绍过的两种例外情况是:
-
其一,使用拷贝初始化时(即使用
=
时),只能提供一个初始值; -
其二,如果提供的是一个类内初始值则只能使用拷贝初始化或使用花括号的形式初始化。
-
第三种特殊的要求是,如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里:
vector<string> v1{"a", "an", "the"};//列表初始化 vector<string> v2("a", "an", "the");//错误
-
-
通常情况下,可以只提供vector对象容纳的元素数量而不用略去初始值。此时库会创建一个值初始化的(value-initialized)元素初值,并把它赋给容器中的所有元素。这个初值由vector对象中元素的类型决定。
-
对这种初始化的方式有两个特殊限制:
- 其一,有些类要求必须明确地提供初始值,必须提供初始的元素值。
- 其二,如果只提供了元素的数量而没有设定初始值,只能使用直接初始化:
-
当使用{}进行初始化时,如果不能进行列表初始化,则会尝试使用直接初始化
vector<string> v8{10, "hi"};//v8有10个"hi"
-
-
-
3.3.2 向vector对象添加元素
- 开始的时候创建空的vector对象,在运行时再动态添加元素,这一做法与C语言及其他大多数语言中内置数组类型的用法不同。特别是如果用惯了C或者Java,可以预计在创建vector对象时顺便指定其容量是最好的。然而事实上,通常的情况是恰恰相反。
- 如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环
-
3.3.3 其他vector操作
-
要使用
size_type
,需首先指定它是由哪种类型定义的。vector对象的类法总是包含着元素的类型:vector<int>::size_type // 正确 vector::size_type // 错误
-
vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素
确保下标合法的一种有效手段就是尽可能使用范围for语句
-
3.4 迭代器介绍
- 3.4.1 使用迭代器
-
end成员返回的迭代器常被称作尾后迭代器(off-the-end iterator)或者简称为尾迭代器(end iterator)
-
那些拥有迭代器的标准库类型使用
iterator
和const_iterator
来表示迭代器的类型:vector<int>::iterator it; // it 能读写 vector<int>的元素 string::iterator it2; // it2 能读写 string 对象中的字符 vector<int>:: const_iterator it3; // it3 只省旨读元素,不宵旨写元素 string::const_iterator it4; // it4只能读字符,不宵旨写字符
const_iterator
和常量指针差不多,能读取但不能修改它 所指的元素值。相反,iterator
的对象可读可写。 -
为了便于专门得到
const_iterator
类型的返回值,C++11新标准引入了两个新函数,分别是cbegin
和cend
。有所不同的是,不论vector对象(或string对象)本身是否是常量,返回值都是const_iterator
。 -
解引用迭代器可获得迭代器所指的对象,如果该对象的类型恰好是类,就有可能希望进一步访问它的成员:
(*it).emtpy()
注意,
(*it).empty()
中的圆括号必不可少为了简化上述表达式,C++语言定义了箭头运算符
->
。箭头运算符把解引用和成员访问两个操作结合在一起,也就是说,it->mem
和(*it).mem
表达的意思相同。 -
任何一种可能改变vector对象容量的操作,比如
push_back
,都会使该vector对象的迭代器失效。
-
- 3.4.2 迭代器运算
- 对于string或vector的迭代器来说,除了判断是否相等,还能使用关系运算符(
<
、<=
、>
、<=
)对其进行比较。 - 只要两个迭代器指向的是同一个容器中的元素或者尾元素的下一位置,就能将其相减,所得结果是两个迭代器的距离。所谓距离指的是右侧的迭代器向前移动多少位置就能追上左侧的迭代器,其类型是名为
difference_type
的带符号整型数。
- 对于string或vector的迭代器来说,除了判断是否相等,还能使用关系运算符(
3.5 数组
-
3.5.1 定义和初始化内置数组
-
数组是一种复合类型。数组的声明形如
a[d]
,其中a
是数组的名字,d
是数组的维度。维度说明了数组中元素的个数,因此必须大于0。数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的。也就是说,维度必须是一个常量表达式 -
定义数组的时候必须指定数组的类型,不允许用
auto
关键字由初始值的列表推断类型。另外和vector
一样,数组的元素应为对象,因此不存在引用的数组。 -
可以对数组的元素进行列表初始化,此时允许忽略数组的维度。
- 如果在声明时没有指明维度,编译器会根据初始值的数量计算并推测出来;
- 如果指明了维度,那么初始值的总数量不应该超出指定的大小。
- 如果维度比提供的初始值数量大,则用提供的初始值初始化靠前的元素,剩下的元素被初始化成默认值
-
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值
一些编译器支持数组的赋值,这就是所谓的编译器扩展(compiler extension)。但一般来说,最好避免使用非标准特性,因为含有非标准特性的程序很可能在其他编译器上无法正常工作。
-
理解复杂的数组声明
int (*Parray)[10] = &arr;//Parray指向一个含有10个整数的数组 int (&arrRef)[10] = arr;//arrRef引用一个含有10个整数的数组
对修饰符的数量并没有特殊限制:
int *(&arry)[10] = ptrs;//arry是数组的引用,该数组含有10个指针
-
-
3.5.2 访问数组元素
- 在使用数组下标的时候,通常将其定义为
size_t
类。size_t是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在cstddef
头文件中定义了size_t
类型,这个文件是C标准库stddef.h
头文件的C++语言版本。
- 在使用数组下标的时候,通常将其定义为
-
3.5.3 指针和数组
-
当使用数组作为一个
auto
变量的初始值时,推断得到的类型是指针而非数组int ia[] = {0,1,2,3,4,5,6,7,8,9};//ia是一个含有10个整数的数组 auto ia2(ia);//ia2是一个整型指针,指向ia的第一个元素 ia2 = 42;//错误:ia2是一个指针,不能用int值给指针赋值
尽管ia是由10个整数构成的数组,但当使用
ia
作为初始值时,编译器实际执行的初始化过程类似于下面的形式:auto ia2 (&ia[0]); //显然 ia2 的类型是 int*
必须指出的是,当使用
decltype
关键字时上述转换不会发生,decltype(ia)
返回的类型是10个整数构成的数组。 -
指针运算
两个指针相减的结果是它们之间的举例。参与运算的两个指针必须指向同一个数组当中的元素。
两个指针相减的结果的类型是一种名为
ptrdiff_t
的标准库类型,和size_t
一样,ptrdiff_t
也是一种定义在cstddef
头文件中的机器相关的类型。因为差值可能为负值,所以ptrdiff_t
是一种带符号类型。上述指针运算同样适用于空指针和所指对象并非数组的指针。在后一种情况下,两个指针必须指向同一个对象或该对象的下一个位置。如果
p
是空指针,允许给p
加上或减去一个值为0的整型常量表达式。两个空指针也允许彼此相减,结果当然是0。 -
下标和指针
虽然标准库类型string和vector也能执行下标运算,但是数组与它们相比还是有所不同。标准库类型限定使用的下标必须是无符号类型,二内置的下标运算无此要求。
int *p = &ia[2]; int k = p[-2]; //k表示的是ia[0]
-
-
3.5.4 C 风格字符串
-
C标准库 String函数
下面列举了C语言标准库提供的一组函数,这些函数可用于操作C风格字符串,它们定义在
cstring
头文件中,cstring
是C语言头文件string.h
的C++版本。strlen(p) 返回p的长度,空字符不计算在内 strcmp(p1,p2) 比较p1和p2的相等性。如果p1==p2,返回0;如果p1>p2,返回一个正值;如果p1<p2,返回一个负值 strcat(p1,p2) 将p2附加到p1之后,返回p1 strcpy(p1,p2) 将p2拷贝给p1,返回p1 传入此类函数的指针必须指向以空字符作为结束的数组
-
比较字符串
两个C风格字符串比较,实际比较的将是指针而非字符串本身
-
-
3.5.5 与旧代码的接口
-
混用string对象和C风格字符串
-
允许使用以空字符结束的字符数组来初始化string对象或为string对象赋值
-
在string对象的加法运算中允许使用以空字符结束的字符数组作为其中一个运算对象(不能两个运算对象都是);在string对象的复合赋值运算中允许使用以空字符结束的字符数组作为右侧的运算对象。
-
不能用string对象直接初始化指向字符的指针。为了完成该功能,string专门提供了一个名为
c_str
的成员函数char *str = s; //错误:不能用string对象初始化char* const char *str = s.c_str(); //正确
c_str
函数的返回值是一个C风格的字符串,也就是说,函数的返回结果是一个指针,该指针指向一个以空字符结束的字符数组,而这个数组所存的数据恰好与那个string对象的一样。结果指针的类型是const char*
,从而确保我们不会改变字符数组的内容。我们无法保证
c_str
函数返回的数组一直有效,如果执行完c_str
函数后程序想一直都能使用其返回的数组,最好将该数组重新拷贝一份。
-
-
使用数组初始化vector对象
不允许使用一个数组为另一个内置类型的数组赋初值,也不允许使用vector对象初始化数组。相反的,允许使用数组来初始化vector对象。要实现这一目的,只需要指明要拷贝区域的首元素地址和尾后地址就可以了。
vector<int> ivec(begin(int_arr), end(int_arr));
-
3.6 多维数组
-
使用范围for语句处理多维数组
要使用范围 for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
int ia[rowCnt][colCnt]; for (auto &row : ia) for (auto &col : row){ col = cnt; ++cnt; }
如果循环中并没有任何写操作,但还是要将外层循环控制变量声明成引用类型,这是为了避免数组被自动转成指针。假设不用引用类型,则循环如下形式:
for (auto row : ia) for (auto col : row)
则程序将无法通过编译。这是因为,像之前一样第一个循环遍历ia的所有元素,注意这些元素实际上是大小为4的数组。因为row不是引用类型,所以编译器初始化row时会自动将这些数组形式的元素转换成指向该数组内首元素的指针。这样得到的row类型就是
int*
,显然内层的循环就不和法了,编译器将试图在一个int*内遍历,这显然和程序的初衷相去甚远。 -
指针和多维数组
当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。
定义指向多维数组的指针时,千万别忘了这个多维数组实际上是数组的数组。
因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针:
int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组 int (*p)[4]= ia; //p指向含有4个整数的数组 p = &ia[2]; //p指向ia的尾元素
随着C++11新标准的提出,通过使用
auto
或者decltype
就能尽可能地避免在数组前面加上一个指针类型了//输出ia中每个元素的值,每个内层数组各占一行 //p指向含有4个整数的数组 for (auto p = ia; p != ia + 3; ++p) { //q指向4个整数数组的首元素,也就是说,q指向一个整数 for (auto q = *p; q != *p + 4; ++q) cout << *q << ' '; cout << endl; }
当然,使用标准库函数
begin
和end
也能实现同样的功能,而且看起来更简洁一些// p指向ia的第一个数组 for (auto p = begin(ia); p != end(ia); ++p) { //q指向内层数组的首元素 for (auto q = begin(*p); q != end(*p); ++q) cout << *q << ' '; //输出q所指的整数值 cout << endl; }
-
类型别名简化多维数组的指针
读、写和理解一个指向多维数组的指针是一个让人不胜其烦的工作,使用类型别名能让这项工作变得简单一点儿,例如:
using int_array = int[4]; typedef int int_array[4]; //等价的typedef声明
第四章 表达式
4.11 类型转换
-
4.11.3 显示转换
一个命名的强制类型转换具有如下形式:
cast-name<type>(expression);
其中,
type
是转换的目标类型而expression
是要转换的值。如果type
是引用类型,则结果是左值。cast-name
是static_cast
、dynamic_cast
、const cast
和reinterpret_cast
中的一种。dynamic_cast
支持运行时类型识别。cast-name
指定了执行的是哪种转换。-
static_cast
任何具有明确定义的类型转换,只要不包含底层
const
,都可以使用static_cast
。double slope = static_cast<double>(j) / i; double *dp = static_cast<double*>(p); // void *p;
-
const_cast
const_cast
只能改变运算对象的底层const
const char *pc; char *p = const_cast<char*>(pc); // 正确:但是通过p写值是未定义的行为
只有
cosnt_cast
能改变表达式的常量属性,使用其他形式的命名强制类型转换改变表达式的常量属性都将引发编译器错误。同样的,也不能用const_cast
改变表达式的类型。 -
reinterpret_cast
reinterpret_cast
通常为运算对象的位模式提供较低层次上的重新解释。int *ip; char *pc = reinterpret_cast<char*>(ip);
我们必须牢记
pc
所指的真实对象是一个int
而非字符,如果把pc
当成普通的字符指针使用就可能在运行时发生错误。例如:string str(pc);
可能导致异常的运行时行为。通常用于父类指针强转为子类指针。
-
第五章 语句
5.6 try语句块和异常处理
-
5.6.1
throw
表达式抛出异常将终止当前的函数,并把控制权转移给能处理该异常的代码
-
5.6.2
try
语句块-
try语句块内声明的变量在块外部无法访问,特别是在
catch
子句内也无法访问 -
每个标准异常类都定义了名为
what
的成员函数,这些函数没有参数,返回值是C风格字符串(const char*)try { ... } catch (runtime_error err) { cout << err.what() << endl; }
-
如果没能找到匹配的
catch
子句,程序将转到名为terminate
的标准库函数,对于那些没有任何try
语句块定义的异常,也按照类似的方式处理。
-
-
5.6.3 标准异常
C++标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在4个头文件中:
-
exception
头文件定义了最通用的异常类exception
。它只报告异常的发生,不提供任何额外信息。 -
stdexcept
头文件定义了几种常用的异常类exception 最常见的问题 runtime_error 只有在运行时才能检测出的问题 range_error 运行时错误:生成的结果超出了有意义的值域范围 overflow_error 运行时错误:计算上溢 underflow_error 运行时错误:计算下溢 logic_error 程序逻辑错误 domain_error 逻辑错误:参数对应的结果值不存在 invalid_argument 逻辑错误:无效参数 length_error 逻辑错误:试图创建一个超出该类型最大长度的对象 out_of_range 逻辑错误:使用一个超出有效范围的值 -
new
头文件定义了bad_alloc
异常类型。 -
type_info
头文件定义了bad_cast
异常类型。
只能以默认初始化的方式初始化
exception
、bad_alloc
和bad_cast
对象,不允许为这些对象提供初始值。其他异常类型的行为则恰好相反:应该使用
string
对象或者C风格字符串初始化这些类型的对象,但是不允许使用默认初始化的方式。当创建此类型对象时,必须提供初始值,该初始值含有错误相关的信息。异常类型只定义了一个名为
what
的成员函数,该函数没有任何参数,返回值是一个指向C风格字符串的const char*
。该字符串的目的是提供关于异常的一些文本信息。what
函数返回的C风格字符串的内容与异常对象的类型有关。如果异常类型有一个字符串初始值,则what返回该字符串。对于其他无初始值的异常类型来说,what
返回的内容由编译器决定。 -
第六章 函数
6.1 函数基础
-
6.0 函数基础
一个典型的函数定义包括:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。
我们通过调用运算符(call operator)来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参(argument)列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。
-
形参和实参
尽管实参和形参存在对应关系,但是并没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。
-
函数的形参列表
函数的形参列表可以为空,但是不能省略。要想定义一个不带形参的函数,最常用的办法是书写一个空的形参列表。不过为了与C语言兼容,也可以使用关键字void表示函数没有形参。
void f1(){}//隐式地定义空形参列表 void f2(void){}//显示地定义空形参列表
-
函数的返回类型
大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值。函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。
-
-
6.1.1 局部对象
-
在C++中,名字有作用域,对象有生命周期
- 名字的作用域是程序文本的一部分,名字在其中可见
- 对象的生命周期是程序执行过程中该对象存在的一段时间
-
在所有函数体之外定义的对象存在于程序的整个执行过程中。此类对象在程序启动时被创建,直到程序结束才会销毁。局部变量的生命周期依赖于定义的方式。
-
自动变量
我们把只存在于块执行期间的对象成为自动对象(automatic object)。当块的执行结束后,块中创建的自动对象就变成未定义的了。
形参是一种自动对象。
内置类型的未初始化局部变量将产生未定义的值。
-
局部静态变量
某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成
static
类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。
-
-
6.1.2 函数声明
函数声明可以省略形参的名字,但写上更容易帮助使用者理解。
函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称为函数原型(function prototype)。
-
在头文件中进行函数声明
建议变量在头文件中声明,在源文件中定义。与之类似,函数也应该在头文件中声明而在源文件中定义。
定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。
-
-
6.1.3 分离式编译
未来允许编写程序时按照逻辑关系将其划分开来,C++语言支持所谓的分离式编译(separate compilation)。分离式编译允许我们吧程序分割到几个文件中去,每个文件独立编译。
-
编译和链接多个源文件
如果我们修改了其中一个源文件,那么只需要重新编译那个改动了的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是
.obj
(windows)或.o
(UNIX)的文件,后缀名的含义是该文件包含对象代码(object code)。-
举例
将
.cpp
编译为对象文档,可以使用g++
加上命令行选项-c
g++ -c NumFooTest.cpp Num.cpp
这行命令会产生
NumFooTest.o
和Num.o
对象文档。然后将它们链接为可执行文档,我们再次使用
g++
命令:g++ NumFooTest.o Num.o -o NumFooTest
这样就会产生可以执行文档
NumFooTest
。如果改变了
NumFooTest.cpp
文档,我们只需要重新编译NumFooTest.cpp
文档就行了g++ -c NumFooTest.cpp
得到NumFooTest.o对象文档。
然后链接为可执行文档:
g++ NumFooTest.o Num.o -o NumFooTest
只编译变动过的文档可以为我们节约编译时间,大多数的IDE会自动帮助我们完成这一部分功能。
-
-
6.2 参数传递
-
6.2.1 传值参数
-
6.2.2 传引用参数
-
使用引用避免拷贝
拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
如果函数无需改变引用形参的值,最好将其声明为常量引用。
-
使用引用形参返回额外信息
-
-
6.2.3 const形参和实参
和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了。当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
void fcn(const int i) {} void fcn(int i) {}//错误:重复定义了fcn(int)
在C++语言中,允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层const被忽略掉了,所以在上面的代码中传入两个fcn函数的参数可以完全一样。因此第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。
-
指针或引用形参与const
形参的初始化方式和变量的初始化方式是一样的,所以回顾通用的初始化规则有助于理解本节知识。我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。
int i = 42; const int *cp = &i; //正确:但是cp不能改变i const int &r = i; //正确:但是r不能改变i const int &r2 = 42; //正确 int *p = cp; //错误:p的类型和cp的类型不匹配 int &r3 = r; //错误:r3的类型和r的类型不匹配 int &r4 = 42; //错误:不能用字面值初始化一个非常量引用
将同样的初始化规则应用到参数传递上可得如下形式:
int i = 0; const int ci = i; string::size_type ctr = 0; reset(&i); //调用形参类型是int*的reset函数 reset(&ci); //错误:不能用指向const int对象的指针初始化int* reset(i); //调用形参类型是int&的reset函数 reset(ci); //错误:不能把普通引用绑定到const对象ci上 reset(42); //错误:不能把普通应用绑定到字面值上 reset(ctr); //错误:类型不匹配,ctr是无符号类型 //正确:find_char的第一个形参是对常量的引用 find_char("Hello world!", 'o', ctr);
-
尽量使用常量引用
-
-
6.2.4 数组形参
-
数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
-
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式。
//尽管形式不同,但这三个print函数是等价的 //每个函数都有一个const int*类型的形参 void print(const int*); void print(const int[]); void print(const int[10]); //这里的维度表示我们期望数组含有多少元素,实际不一定
-
当编译器处理对
print
函数的调用时,只检查传入的参数是否是const int*
类型。int i = 0,j[2] = {0, 1}; print(&i); //正确:&i的类型是int* print(j); //正确:j转换成int*并指向j[0]
-
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。
-
使用标记指定数组长度
典型示例为C风格字符串,以
\0
结尾。 -
使用标准库规范
管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发。使用该方法,我们可以按照如下形式输出元素内容:
void print (const int *beg,const int *end) { //输出beg到end之间(不含end)的所有元素 while (beg != end) cout << *beg++ << endl; //输出当前元素并将指针向前移动一个位置 }
-
显式传递一个表示数组大小的形参
-
-
数组形参和const
当数组形参不需要改变时,定义成指向const的指针
-
数组引用形参
C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上
void print(int (&arr)[10]) { for (auto elem : arr) cout << elem << endl; }
&arr
两端的括号必不可少f(int &arr[10]) //错误:将arr声明成了引用的数组 f(int (&arr)[10]) //正确:arr是具有10个整数的整数数组的引用
因为数组的大小是构成数组类型的一部分,所以只要不超过维度,在函数体内就可以放心地使用数组。但是,这一用法也无形中限制了print函数的可用性,我们只能将函数作用于大小为10的数组。
int i = 0,j[2]={0,1}; int k[10] = {0,1,2,3,4,5,6,7,8,9}; print(&i); //错误:实参不是含有10个整数的数组 print(j); //错误:实参不是含有10个整数的数组 print(k); //正确:实参是含有10个整数的数组
-
传递多维数组
和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略。
void print(int (*matrix)[10], int rowSize) {}//括号不能省略 //等价定义 void print(int matrix[][10], int rowSize) {}
-
-
6.2.5 main:处理命令行选项
假定main函数位于可执行文件prog之内,我们可以想程序传递下面的选项。
prog -d -o ofile data0
int main(int argc, char *argv[]) {} //等价定义 int main(int argc, char **argv) {}
以上面提供的命令行为例,
argc
应该等于5,argv
应该包含如下的C风格字符串。argv[0] = "prog";//或者argv[0]也可以指向一个空字符串 argv[1] = "-d"; argv[2] = "-o"; argv[3] = "ofile"; argv[4] = "datao"; argv[5] = 0;
当使用
argv
中的实参时,一定要记得可选的实参从argv[1]
开始;argv[0]
保存程序的名字,而非用户输入。 -
6.2.6 含有可变形参的函数
有时我们无法提前预知应该向函数传递几个实参。例如,我们想要编写代码输出程序产生的错误信息,此时最好用同一个函数实现该项功能,以便对所有错误的处理能够整齐划一。然而,错误信息的种类不同,所以调用错误输出函数时传递的实参也不相同。
-
为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法。
- 如果所有的实参类型相同,可以传递一个名为
initializer_list
的标准库类型. - 如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板。
- 如果所有的实参类型相同,可以传递一个名为
-
initializer_list形参
initializer_list
是一种标准库类型,用于表示某种特定类型的值的数组。initializer_list
类型定义在同名的头文件中,它提供的操作如下initializer_list lst; 默认初始化;T类型元素的空列表 initializer_list lst{a,b,c…}; lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const lst2(lst) lst2 = lst 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素 lst.size() 列表中的元素数量 lst.begin() 返回指向lst中首元素的指针 lst.end() 返回指向lst中尾元素下一位置的指针 如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内。
void error_msg(initializer_list<string> il); //expected和actual是string对象 error_msg("functionX", expected, actual});
-
省略符形参
省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为
varargs
的C标准库功能。通常,省略符形参不应用于其他目的。你的C编译器文档会描述如何使用varargs。省略符形参应该仅仅用于C和C++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种
void foo(parm_list, ...); void foo(...);
第一种形式指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。省略符形参所对应的实参无须类型检查。在第一种形式中,形参声明后面的逗号是可选的。
在函数体内,需要使用
stdarg.h
头文档中的宏和函数对省略符形参进行访问。主要使用的宏和函数有:va_list
:定义一个指向可变参数列表的变量。va_start
:将可变参数列表初始化为具体的参数。va_arg
:获取可变参数列表中的下一个参数。va_end
:清理可变参数列表。
举例:
#include <stdarg.h> int sum(int count, ...) { va_list args; int total = 0; va_start(args, count); // 初始化可变参数列表 for (int i = 0; i < count; i++) { int num = va_arg(args, int); // 获取下一个参数 total += num; } va_end(args); // 清理可变参数列表 return total; } int main() { int result = sum(3, 1, 2, 3); // 调用sum函数,传递3个整数参数 return 0; }
-
6.3 返回类型和return语句
-
6.3.1 无返回值函数
返回void的函数可以没有return语句,因为函数最后会隐式执行return。
返回void的函数可以return一个void值。
return f();//void f();
-
6.3.2 有返回值函数
在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。很多编译器都无法发现此类错误。
-
值是如何被返回的
返回一个值的方式和初始化一个变量或形参的方式完全一样;返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
同其他引用类型一样,如果函数返回引用,则该引用仅是它所引对象的一个别名。举个例子来说明,假定某函数挑出两个string形参中较短的那个并返回其引用:
//挑出两个string对象中较短的那个,返回其引用 const string &shorterString(const string &s1l, const string &s2) { return s1.size() <= s2.size() ? s1 : s2; }
其中形参和返回类型都是
const string
的引用,不管是调用函数还是返回结果都不会真正拷贝string对象。 -
不要返回局部对象的引用或指针
-
返回类类型的函数和调用运算符
和其他运算符一样,调用运算符也有优先级和结合律。调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律。因此,如果函数返回指针、引用或类的对象,我们就能使用函数调用的结果访问结果对象的成员。
例如,我们可以通过如下形式得到较短string对象的长度:
//调用string对象的size成员,该string对象是由shorterstring函数返回的 auto sz = shorterString(s1, s2).size();
因为上面提到的运算符都满足左结合律,所以
shorterString
的结果是点运算符的左侧运算对象,点运算符可以得到该string对象的size成员,size又是第二个调用运算符的左侧运算对象。 -
引用返回左值
函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数得到左值,其他换回类型得到右值。
简而言之,就是返回非常量引用的函数可以被放在
=
左边。 -
列表初始化返回值
C++11新标准规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。
-
主函数main的返回值
我们允许main函数没有return语句直接结束。如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。
main函数的返回值可以看做是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。为了使返回值与机器无关,
cstdlib
头文件定义了两个预处理变量,我们可以使用者两个变量分别表示成功与失败:int main() { if (some_failure) return EXIT_FAILURE; else return EXIT_SUCCESS; }
因为它们是预处理变量,所以既不能在前面加上
std::
,也不能在using
声明中出现。 -
递归
如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。
在递归函数中,一定有某条路径是不包含递归调用的;否则,函数将”永远”递归下去,换句话说,函数将不断地调用它自身直到程序栈空间耗尽为止。我们有时候会说这种函数含有递归循环(recursion loop)。
-
-
6.3.3 返回数组指针
因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是有一些方法可以简化这一任务,其中最直接的方法是使用类型别名:
typedef int arrT[10]; //arrT是一个类型别名,它表示的类型是含有10个 //整数的数组 using arrT = int[10]; //arrT的等价声明 arrT* func(int i); //func返回一个指向含有10个整数的数组的指针
-
声明一个返回数组指针的函数
要想在声明
func
时不使用类型别名,我们必须牢记被定义的名字后面数组的维度: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类型 -
使用尾置返回类型
在C++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型(trailing return type)。任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并以一个
->
符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto://func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组 auto func(int i) -> int(*)[10];
因为我们把函数的返回类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针,并且该指针指向了含有10个整数的数组。
-
使用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 函数重载
main函数不能重载。
不允许两个函数处理除了返回类型外其他所有的要素都相同。
别名也视为相同类型形参。
-
重载和const形参
一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。
如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的。
当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
-
cosnt_cast
和重载返回const引用函数重载返回非const引用函数可以使用
cosnt_cast
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 stirng&>(s2)); return const_cast<string&>(r); }
-
调用重载的函数
调用重载函数时有三种可能的结果:
- 编译器找到一个与实参最佳匹配(best match)的函数,并生成调用该函数的代码。
- 找不到任何一个函数与调用的实参匹配,此时编译器发出无匹配(no match)的错误信息。
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择。此时也将发生错误,称为二义性调用(ambiguous call)。
-
6.4.1 重载与作用域
在局部作用域声明函数后,编译器就会忽略掉外层作用域中的同名实体。
6.5 特殊用途语言特性
-
6.5.1 默认实参
- 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值
- 当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。
- 局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参。
-
6.5.2 内联函数和
constexpr
函数-
内联函数
内联函数可避免函数调用的开销
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
-
constexpr
函数constexpr
函数是指能用于常量表达式的函数定义
constexpr
函数的方法与其他函数类似,不过要遵循几项约定:函数的返回类型及所有形参的类型都得是字面值类型,而且函数体重必须有且只有一条return
语句。因为编译器能在程序编译时验证
constexpr
函数返回的是常量表达式,所以可以用函数初始化constexpr
类型的变量。执行该初始化任务时,编译器把对
constexpr
函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr
函数被隐式地指定为内联函数。constexpr
函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。例如,函数中可以有空语句、类型别名以及using
声明。允许
constexpr
函数的返回值并非一个常量://如果arg是常量表达式,则scale(arg)也是常量表达式,反之则不然 constexpr int new_sz() { return 42; } constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
-
把内联函数和
constexpr
函数放在头文件内和其他函数不一样,内联函数和
constexpr
函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过,对于某个给定的内联函数或者constexpr
函数来说,它的多个定义必须完全一致。基于这个原因,内联函数和constexpr
函数通常定义在头文件中。
-
-
6.5.3 调试帮助
-
assert
预处理宏assert(expr);
首先对
expr
求值,如果表达式为假(即0),assert
输出信息并终止程序的执行。如果表达式为真(即非0),assert
什么也不做。assert
宏定义在cassert
头文件中,预处理名字有预处理器而非编译器管理,因此我们可以直接使用预处理名字而无需提供using
声明。也就是说,我们应该使用assert
而不是std::assert
,也不需要为assert
提供using
声明。和预处理变量一样,宏名字在程序内必须唯一。含有cassert头文件的程序不能再定义名为
assert
的变量、函数或者其他实体。在实际编程过程中,及时我们没有包含cassert
头文件,也最好不要为了其他目的使用assert
。很多头文件都包含了cassert
,这就意味着即使你没有直接包含cassert
,它也很有可能通过其他途径包含在你的程序中。 -
NDEBUG
预处理变量assert
的行为依赖于一个名为NDEBUG
的预处理变量的状态。如果定义了NDEBUG
,则assert
什么也不做。默认状态下没有定义NDEBUG
,此时assert
将执行运行时检查。__func__
是const char
的一个静态数组,用于存放函数的名字。编译器为每个函数都定义了__func__
。除了C++编译器定义的
__func__
之外,预处理器还定义了另外4个对于程序调试很有用的名字:FILE 存放文件名的字符串字面值 LINE 存放当前行号的整型字面值 TIME 存放文件编译时间的字符串字面值 DATE 存放文件编译日期的字符串字面值
-
6.6 函数匹配
-
确定候选函数和可行函数
函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数(candidate function)。候选函数具备两个特征:一是与被调用的函数同名,二是其声明在调用点可见。在这个例子中,有4个名为f的候选函数。
第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些新选出的函数称为可行函数(viable function)。可行函数也有两个特征:一是其形参数量与本次调用提供的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
-
寻找最佳匹配
函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。“最匹配”基本思想是,实参类型与形参类型越接近,它们匹配得越好。
-
含有多个形参的函数匹配
选择可行函数的方法和只有一个实参时一样,编译器选择那些形参数量满足要求且实参类型和形参类型能够匹配的函数。接下来,编译器依次检查每个实参以确定哪个函数是最佳匹配。如果有且只有一个函数满足下列条件,则匹配成功:
- 该函数每个实参的匹配都不劣于其他可行函数需要的匹配。
- 至少有一个实参的匹配优于其他可行函数提供的匹配。
如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用是错误的。编译器将报告二义性调用的信息。
PS:调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
-
6.6.1 实参类型转换
为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下所示:
- 精确匹配,包括以下情况:·实参类型和形参类型相同。
- 实参从数组类型或函数类型转换成对应的指针类型。
- 向实参添加顶层const或者从实参中删除顶层const。
- 通过const转换实现的匹配。
- 通过类型提升实现的匹配。
- 通过算术类型转换或指针转换实现的匹配。
- 通过类类型转换实现的匹配。
-
需要类型提升和算术类型转换的匹配
分析函数调用前,我们应该知道小整型一般都会提升到
int
类型或更大的整数类型。void ff(int); void ff(short); ff('a'); // char提升成int;调用f(int) void manip(long); void manip(float); manip(3.14); // 错误:二义性调用
- 精确匹配,包括以下情况:·实参类型和形参类型相同。
6.7 函数指针
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"); // 一个等价的调用
在指向不同函数类型的指针间不存在转换规则。
-
重载函数的指针
当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。
void ff(int*); void ff(unsigned int); void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)
-
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用:
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);
正如
useBigger
的声明语句所示,直接使用函数指针类型显得冗长而烦琐。类型别名和decltype
能让我们简化使用了函数指针的代码:// Func和Func2是函数类型 typedef bool Func(const string&, const string&); typedef decltype(lengthCompare) Func2; // 等价的类型 // FuncP和FuncP2是指向函数的指针 typedef bool (*FuncP)(const string&, const string&); typedef decltype(lengthCompare) *FuncP2; // 等价的类型
-
返回指向函数的指针
和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。与往常一样,要想声明一个返回函数指针的函数,最简单的办法是使用类型别名:
using F = int(int*, int); // F是函数类型,不是指针 using PF = int(*)(int*, int); // PF是指针类型
其中我们使用类型别名将
F
定义成函数类型,将PF
定义成指向函数类型的指针。必须时刻注意的是,和函数类型的形参不一样,返回类型不会自动地转换成指针。我们必须显式地将返回类型指定为指针:PF f1(int); // 正确:PF是指向函数的指针,f1返回指向函数的指针 F f1(int); // 错误:F是函数类型,f1不能返回一个函数 F *f1(int); // 正确:显示地指定返回类型是指向函数的指针 int (*f1(int))(int*, int); // 等价定义 auto f1(int) -> int(*)(int*, int); // 等价定义
-
将
auto
和decltype
用于函数指针类型如果我们明确知道返回的函数是哪一个,就能使用decltype简化书写函数指针返回类型的过程。
string ::size_type sumLength(const string&,const string&); string ::size_type largerLength(const string&,const string&); // 根据其形参的取值,getFcn函数返回指向sumLength或者largerLength的指针 decltype(sumLength) *getFcn(const string &);
声明
getFcn
唯一需要注意的地方是,牢记当我们将decltype
作用于某个函数时,它返回函数类型而非指针类型。因此,我们显式地加上*以表明我们需要返回指针,而非函数本身。
本文作者:Apricity_chen
本文链接:https://www.cnblogs.com/apricity-chen/p/16975818.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步