【C++基础】1. 基本使用

1. 基本注意点

  • C++源文件的后缀可以是:.cc、.cxx、.cpp、.cp、.C
  • iostream标准库包含两个基础类型istream和ostream,分别表示输入流和输出流。一个流就是一个字符序列,从IO设备读出或写入IO设备。“流”表达的意思是:随着时间的推移,字符是顺序生成或消耗的
  • 标准库定义了4个IO对象,istream类型的对象有:标准输入对象cin,搭配输入运算符>>使用。ostream类型的对象有:标准输出对象cout,标准错误cerr,日志信息clog,搭配输出运算符<<使用
  • endl为操纵符,写入endl的效果是结束当前行,并将设备关联的缓冲区中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是停留在内存中等待写入流
  • 当使用一个istream对象作为条件时,其效果是检查流的状态,如果流是有效的则检测成功,当遇到文件结束符(Windows系统为Ctrl+Z然后按Enter键)或无效输入(如读入值不为整数)时,istream对象会使条件变为假
int sum=0,value=0;
while(std::cin>>value)
	sum+=value;
std::cout<<sum<<std::endl;

2. 变量和基本类型

  • C++中的基本内置类型:整型(包括字符和布尔类型)、浮点型、空类型(void)
  • 当一个算术表达式中既有无符号数又有int值时,int值会被转换成无符号数(二进制转换),需要避免混用带符号类型和无符号类型。无符号数之间的减法不会产生负数,会得到二进制转换后的结果
  • 当对象在创建时获得了一个特定的值,称为初始化。把对象的当前值擦除,用一个新值代替,称为赋值。4种方法都可以将int变量a初始化为0:① int a=0;② int a={0};③ int a(0);④ int a{0}
  • 使用花括号初始化变量称为列表初始化,C++11之后,也可以使用花括号进行赋值。如果使用列表初始化且初始值存在丢失信息的风险,则编译器将报错
  • 如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予默认值。对于内置类型的变量,在函数体外被默认初始化为0,在函数体内不被初始化,此时改变量的值未定义,如果试图拷贝或访问该值会报错。非内置类型的变量自己决定能否默认初始化,以及默认初始化的结果,string类默认初始化为空串,自定义类默认初始化将执行无参构造函数
  • C++通过区分声明定义来支持分离式编译,声明使得名字为程序所知,定义创建与名字关联的实体。一个文件如果想使用别处定义的名字则必须包含对哪个名字的声明,变量只能被定义一次,但可以被多次声明
  • 声明规定了变量的类型和名字,定义还申请了存储空间,也可能为变量赋一个初始值。如果想声明一个变量而非定义它,就在变量名前加关键字extern,而且不要显式初始化变量。不能在函数体内部初始化一个由extern标记的变量
extern int i;	//声明
int i;			//声明并定义
extern int i=0;	//定义
  • C++的标识符由字母、数字、下划线组成,必须以字母或下划线开头,用户自定义的标识符中不能连续出现两个下划线,也不能以下划线紧连大写字母开头,定义在函数体外的标识符不能以下划线开头
  • 默认状态下,const对象仅在文件内有效,编译器会在编译的过程中把用到const变量的地方都替换成对应的值。如果想在多个文件之间共享const对象,则不管声明还是定义都要添加extern关键字
  • 允许一个常量引用绑定非常量的对象const T& t,此时常量引用不能修改绑定对象的值,但非常量引用不能绑定常量对象。指向常量的指针不能用于修改其所指对象的值const T* t,常量指针不能改变指针本身T* const t
  • 指针本身是一个对象,它又可以指向另一个对象,顶层const表示指针本身是一个常量,底层const表示指针所指的对象是一个常量。更一般的,顶层const可以表示任意数据类型的对象是常量,底层const则与指针和引用等复合类型有关,用于声明引用的const都是底层const
  • 当执行拷贝操作时,顶层const不受影响,拷贝操作并不会改变被拷贝对象的值。而底层const对拷贝有限制,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型能够转换,非常量可以转为常量,反之不行
  • 常量表达式指值不会改变并且在编译过程就能得到计算结果的表达式,字面值和const对象属于常量表达式。一个对象或表达式是否是常量表达式由它的数据类型和初始值共同决定,C++11允许将变量声明为cosntexpr类型以便由编译器来验证变量的值是否是一个常量表达式,声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化
  • 能在编译时就得到计算的类型称为字面值类型,算术类型、引用、指针都属于字面值类型,自定义类、string类型等不属于字面值类型,不能被定义成constexpr。引用和指针被定义成constexpr时初始值受到限制,constexpr指针和引用的初始值必须是存储与某个固定地址中对象或nullptr(对于指针),函数体内的变量一般不存放在固定地址中(static除外),constexpr指针和引用不能指向这样的变量,函数体之外的对象地址固定不变,能用来初始化constexpr指针和引用
  • constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指向的对象无关,即constexpr把它所定义的对象置为了顶层const。与普通指针类似,constexpr指针既可以指向常量也可以指向非常量(固定地址)
const int* p=nullptr;		//指向常量的指针
constexpr int* q=nullptr;	//常量指针
  • 使用关键字typedefusing定义类型别名,对于包含复合类型和常量的类型别名,如typedef char* pstring,使用const pstring cs时,表示常量指针,而非指向常量的指针
typedef double wages;
using SI=Sales_items;
  • C++11引入了auto类型说明符,让编译器通过初始值来推断变量的类型,auto定义的变量必须有初始值。auto会忽略掉顶层const,同时底层const会保留下来,如果希望推断出的类型是一个顶层const,需要显式指出。如果希望推断出的类型是一个引用,需要显式使用auto&
  • C++11引入的第二种类型说明符decltype,用于选择并返回操作数的数据类型,在此过程中,编译器分析表达式并得到它的类型,但不计算表达式的值。与auto不同,decltype会保留变量的顶层const以及引用类型。如果表达式的内容是解引用操作,则decltype将得到引用类型,如果decltype的变量名加上了一对括号decltype((value)),则表示引用类型
  • 自定义类型使用structclass,C++11可以为类的数据成员提供一个类内初始值,创建对象时,类内初始值用于初始化数据成员,没有初始值的成员将被默认初始化,类内初始值可使用花括号或等号,但不能使用圆括号

3. 字符串、向量和数组

  • 读写string对象的操作:① os<<s:将s写到输出流os中,返回os;② is>>s:从is中读取字符串赋给s,字符串以空白分隔,返回is,字符串会自动忽略开头的空白;③ getline(is,s):从is中读取一行赋给s,返回is
  • stringvectorsize()方法返回的都是对应的size_type类型(string::size_typevector<T>::size_type),该类型是一个与机器无关的无符号整型
  • 在头文件<cctype>(或<ctype.h>)中,定义了一组标准库函数用于处理string对象中的字符,可以判断每个字符是否为数字、字母、大小写等,也可以进行大小写的转换。通过使用范围for语句可将string对象转换成大写,或使用下标执行迭代
for(auto& c:s) c=toupper(c);
for(string::size_type i=0;i!=s.size();++i) s[i]=toupper(s[i]);
  • vector<T> v(n,val)初始化了n个重复元素,每个元素的值都是valvector<T> v(n)指定了元素数量而未指定初始值,此时会对每个元素执行值初始化,对于内置类型,值初始化为0,对于非内置类型,由类执行默认初始化,若元素类型不支持默认初始化,则必须提供初始元素值。若使用花括号进行初始化,默认为列表初始化,当花括号中的类型无法执行列表初始化时,会尝试执行直接初始化(通过圆括号初始化)
  • 使用vector的成员函数push_back(T)向vector中添加元素,与C和Java不同,C++先创建一个空的vector对象,然后动态添加元素,比在创建vector对象的同时指定容量的效率更高,只有一种情况例外,就是vector中所有元素的值都一样。如果循环体内包含有向vector对象添加元素的语句,则不能使用范围for循环for(auto& i:v)。下标运算符可用于访问vector中已存在元素,但不能用于添加元素
  • 除了可以使用下标运算符来访问string和vector对象外,还有另一种更通用的机制迭代器,所有标准库容器都可以使用迭代器,但只有少数几种才支持下标运算符,string对象不属于容器类型,但是支持很多与容器类型类似的操作。迭代器与指针类似,提供了对对象的间接访问,迭代器有有效和无效之分,有效的迭代器指向某个元素,或指向容器中尾元素的下一个位置,其他情况都属于无效迭代器
  • 有迭代器的类型都拥有成员函数begin()end(),其中begin()返回指向第一个元素的迭代器,end()返回指向容器末尾元素的下一个位置的迭代器,又称作尾后迭代器,该迭代器没有实际意义,仅是一个标记,表示已经处理完容器中所有的元素,如果容器为空,begin()end()返回同一个迭代器
  • 标准容器的迭代器运算符:① *iter:返回迭代器所指元素的引用;② iter->mem解引用迭代器并获取该元素的成员;③ ++iter/--iter:令迭代器指向容器中的上/下一个元素;④ iter1==iter2/iter1!=iter2:判断两个迭代器是否相等,若指向同一个元素或是同一个容器的尾后迭代器,则相等,反之不相等
  • 由于尾后迭代器并不实际指向某个元素,所以不能对其进行递增和解引用的操作。所有的标准容器的迭代器都定义了==!=运算符,大多数没有定义<运算符,因此使用for循环遍历迭代器时,要使用!=做条件判断
for(auto it=s.begin();it!=s.end();++it)
  • 迭代器包括iteratorconst_iterator类型,const_iterator类似于常量指针,能读取但不能修改其所指的元素值,若迭代器对象是一个常量,则只能使用const_iterator,若不是常量,则两者都可以使用。begin()end()返回的迭代器类型由对象是否是常量决定,若想固定返回const_iterator类型,可使用cbegin()cend()
  • string和vector的迭代器提供了除==!=外的更多种运算符:iter+niter-niter+=niter-=niter1-iter2>>=<<=。迭代器之间的距离的类型是名为difference_type的带符号整型数
  • 数组是一种类似于vector的数据结构,数组的大小确定不变,不能随意向数组中增加元素。数组的声明为:T a[d];,其中a是数组的名字,T是数组中变量的类型,d是数组的维度,说明了数组中元素的个数,必须大于0,数组中元素的个数也属于数组类型的一部分,编译的时候维度应该是已知的,因此维度必须是一个常量表达式
  • 和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,则默认初始化会令数组含有未定义的值。定义数组时必须指定数组的类型,不能使用auto关键字由初始值的列表推断类型。和vector一样,数组的元素应为对象,因此不存在引用的数组
  • 可以对数组的元素进行列表初始化,此时允许忽略数组的维度。如果在声明时没有指定维度,编译器会根据初始值的数量推测,如果指明了维度,那么初始值的总数量不应该超出指定的大小,若指定维度比提供的初始值数量大,剩下的元素被初始化为默认值。字符数组可以用字符串字面值初始化,此时字符数组的最后一个字符为空字符
int a[10]={};					//10个整数全部初始化为0
char a1[]={'C','+','+','\0'};	//列表初始化,显式加入空字符
char a2[]="C++";				//自动添加末尾的空字符
const char a3[3]="C++";			//错误!没有空间可存放空字符
  • 不能将数组的内容拷贝给其他数组作为初始值,也不能用数组为其他数组赋值(有些编译器支持数组的赋值,但并非标准特性)
  • 可以定义一个存放指针的数组,不能定义存放引用的数组。数组本身也是对象,所以允许定义数组的指针和数组的引用
int *a[10];			//存放整形指针的数组
int &a[10];			//错误!不存在存放引用的数组
int (*a1)[10]=&a2;	//指向整型数组的指针
int (&a1)[10]=a2;	//整形数组的引用
  • 很多用到数组名字的地方,编译器都会自动将其替换为一个指向数组首元素的指针:int* p=a;等价于int* p=&a[0];,在一些情况下数组的操作实际上是指针的操作,当数组作为一个auto变量的初始值时,推断得到的类型是指针而非数组:auto a2(a1);,而使用decltype关键字时得到的仍是数组类型decltype(a)
  • 指向数组的指针同时也是数组的迭代器,vector和string迭代器支持的运算,数组的指针全部支持,通过获取数组尾元素之后那个不存在的元素的地址来得到数组的尾指针int* e=&a[d];,但这种方法极易出错,C++新标准引入了begin(a)end(a)函数,可返回数组首元素的指针和尾元素的下一个位置的指针,这两个函数定义中<iterator>头文件中
  • 字符串字面值是C风格字符串,C风格字符串不是一种类型,而是为了表达和使用字符串而形成的一种约定俗成的写法,将字符串放在字符数组中并以空字符'\0'结束。尽管C++支持C风格字符串,但在C++程序中尽量不要使用,因为使用不方便且易引发程序漏洞。有关两种风格字符串的转换使用见另一篇博客:C++字符串
  • 不允许使用一个数组为另一个数组赋初值,也不允许使用vector对象初始化数组,但可以使用数组来初始化vector对象:vector<int> v(begin(a),end(a));
  • 严格来说,C++中没有多维数组,通常所说的多维数组其实是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针
int a[3][4];		//大小为3的数组,每个元素是大小为4的整形数组
int (*p)[4]=a;		//p指向a的第一个元素
p=&a[1];			//p指向a的第二个元素

4. 表达式

  • 表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,把一个运算符和一个或多个运算对象组合起来可以生成较复杂的表达式。C++定义了一元运算符和二元运算符,还有一个三元运算符,函数调用也是一种特殊的运算符,它对运算对象的数量没有限制。有些符号既可以作为一元运算符也能作为二元运算符,如*
  • C++的表达式要么是右值,要么是左值。在C语言中,左值可以位于赋值语句的左侧,而右值不能。在C++中,二者的区别更为复杂,一个左值表达式的结果是一个对象或一个函数,某些左值(如常量)不能放在赋值语句左侧,某些表达是的结果是对象但却是右值。当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(在内存中的位置)
  • 不同的运算符需要不同的运算对象,有的需要左值,有的需要右值。返回值也有差异,有的得到左值结果,有的得到右值结果。在需要右值的地方可以用左值来代替,但不能用把右值当成左值使用,当一个左值被当成右值使用时,实际使用的是它的内容。以下几种运算符需要用到左值:
  • 赋值运算符=需要一个非常量左值作为其左侧运算对象,得到的结果也是一个左值
  • 取地址符&作用于一个左值,返回一个指向该左值对象的指针,这个指针是一个右值
  • 内置解引用运算符*、下标运算符[]、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值
  • 内置类型和迭代器的递增递减运算符作用于左值对象,其前置版本所得的结果也是左值
  • 使用关键字decltype作用于表达式时,如果该表达式的结果是一个左值,decltype得到的结果是一个引用类型。对于类型为int*的对象p,decltype(*p)的结果是int&decltype(&p)的结果是int**
  • 运算符的优先级规定了运算对象的组合方式,但没有规定运算对象按什么顺序求值,在大多数情况下不会明确求值的顺序,如int i=f()*g();,其中函数运算一定会在乘法前被调用,但无法知道哪个函数会先调用。如果表达式修改了一个对象,还在其他地方使用了该对象,会引发错误并产生未定义的行为,如cout<<i<<++i<<endl;表达式是未定义的
  • 有4种运算符明确规定了运算对象的求值顺序,逻辑与&&运算符(左侧为真时才会求右侧的值)、逻辑或||运算符,条件?:运算符、逗号,运算符
  • 处理复合表达式的经验准则:① 在拿不准运算符优先级时使用括号强制规定组合方式;② 若表达式种修改了某个对象的值,在其他地方不要再使用该对象(*++iter可以正常使用)
  • 递增和递减运算符有前置版本和后置版本,这两种运算符必须作用于左值运算对象,前置版本将对象本身作为左值返回,后置版本将对象原始值的副本作为右值返回。由于后置版本多一步存储操作,故C++尽量使用前置版本
  • 位运算符作用于整数类型的运算对象,把运算对象看成是二进制位的集合,包括:① ~:位求反;② <<:左移;③ >>:右移;④ &:位与;⑤ ^:位异或;⑥ |:位或。位运算对象可以是带符号的,也可以时候无符号的,但如何处理符号位依赖于机器,所以一般仅将位运算符作用于无符号类型
  • sizeof运算符返回一条表达式或一个类型名所占的字节数,所得的值是一个size_t类型的常量表达式,该运算符有两种使用方式:① sizeof(T);:返回类型T的大小;② sizeof t;:返回对象t所对应的类的大小。该运算符作用于数组时返回整个数组的大小,作用于string或vector时只返回固定部分的大小,不会计算内部元素所占空间
  • 逗号运算符含有两个运算对象,按照从左到右的顺序依次求值,将左侧的求值结果丢弃,以右侧表达式的值作为最后的结果,如果右侧运算对象是左值,那么最终的求值结果也是左值
  • C++中,如果两种类型可以相互转换,那么它们就是关联的。编译器自动转换对象的类型称为隐式转换,隐式转换会发生的情况:① 大多数表达式中,比int类型小的整型值会先提升为较大的整数类型;② 在条件中,非布尔值转为布尔类型;③ 初始化过程中,初始值转换成变量的类型;赋值语句中,右侧运算对象转换成左侧运算对象的类型;④ 如果算术运算或关系运算的对象有多种类型,会转换成同一种类型;⑤ 函数调用时会发生类型转换
  • 无符号类型转换:如果一个运算对象是无符号类型,另一个运算对象是带符号类型,且其中的无符号类型不小于带符号类型,那么带符号的运算对象转为无符号的。如unsigned intint运算,int类型的运算对象会转为unsigned int类型,若int型的值为负,会带来副作用。若带符号类型大于无符号类型,此时转换的结果依赖于机器
  • 除了算术转换之外还有几种隐式类型转换:① 数组转换成指针:在大多数用到数组的表达式中,数组自动转换成指向数组首元素的指针,当数组被用作decltype关键字的参数、作为取地址符&、sizeoftypeid等运算符的运算对象时、通过引用来初始化数组时,上述转换不会发生;② 指针的转换:常量整数值0或字面值nullptr能转换成任意指针类型,指向任意非常量的指针能转换成void*,指向任意对象的指针能转换成const void*;③ 转换成布尔类型:如果指针或算术类型的值为0,转换结果时false,否则转换结果时true;④ 转换成常量:允许将指向非常量类型的指针或引用转换成指向相应常量类型的指针或引用,而不允许相反的转换;⑤ 类类型定义的转换:类类型能定义由编译器自动执行的转换,不过编译器每次只能执行一种类类型的转换,while(cin>>s)istream类型自动转为布尔类型,转换规则由IO库定义,若最后一次读取成功,转换得到true,若最后一次读入不成功,转换得到false

显式转换

显式转换又称为强制类型转换(cast),分为:① 命名的强制类型转换;② 旧式的强制类型转换

一个命名的强制类型转换具有如下形式:

cast-name<type>(expression);

其中,type是转换的目标类型,若type是引用类型,则结果是左值;expression是要转换的值;cast-namestatic_castdynamic_castconst_castreinterpret_cast中的一种,指定了执行的是哪种转换。

  • dynamic_cast支持运行时识别
  • static_cast:任何具有明确定义的类型转换,只要不包含底层const,都可以使用static_cast。如将运算对象强制转换成double类型使表达式执行浮点数除法:double d = static_cast<double>(j)/i;,static_cast常用于把一个较大的算术类型赋值给较小的类型,也可用于转换编译器无法自动执行的类型转换,如找回存在于void*指针中的值:void* p = &d; double *dp = static_cast<double*>(p);
  • const_cast:只能改变运算对象的底层const,const char *pc; char *p = const_cast<char*>(pc);,去掉对象的const性质后,编译器就不再组织对该对象进行写操作了,但是如果对象本身是一个常量,执行写操作会产生未定义的后果。const_cast常用于有函数重载的上下文中
  • reinterpret_cast:通常为运算对象的位模式提供较低层次上的重新解释,如int *ip; char *pc = reinterpret_cast<char*>(ip);,必须牢记pc所指的只是对象是一个int而非字符,若使用string str(pc);就会产生运行时报错。

旧式的强制类型转换:在早期版本的C++语言中,显式地进行强制类型转换包含两种形式:① 函数形式地强制类型转换:type(expr);;② C语言风格地强制类型转换:(type)expr;。根据所涉及地类型不同,旧式地强制类型转换分别具有与static_cast、const_cast、reinterpret_cast相似的行为。与命名的强制类型转换相比,旧式的强制类型转换从表现形式上不清晰,转换过程出问题后追踪更加困难

5. 语句

  • 表达式语句:一个表达式末尾加上分号就变成了表达式语句,表达式语句的作用是执行表达式并丢掉求值结果
  • 空语句:空语句是最简单的语句,只含有一个单独的分号。如果在程序的某个地方,语法上需要一条语句但逻辑上不需要,此时应使用空语句,如当循环的全部工作在条件部分就可以完成时使用空语句
  • 复合语句:复合语句指用花括号括起来的语句和声明序列,也被称作块,一个块就是一个作用域,块不以分号结束。内部没有任何语句的一对花括号为空块,空快的作用等价于空语句

6. 函数

6.1 函数基础

  • 一个典型的函数定义包括以下部分:返回类型、函数名、由0个或多个形参组成的列表、函数体。通过调用运算符来执行函数,调用运算符的形式是一对圆括号,作用于一个表达式,该表达式是函数或指向函数的指针,圆括号内是实参列表,用实参初始化函数的形参,调用表达式的类型就是函数的返回类型
  • 函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数,此时主调函数的执行被暂时中断,被调函数开始执行
  • 实参是形参的初始值,第一个实参初始化第一个形参,第二个实参初始化第二个形参。尽管实参与形参存在对应关系,但并没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值
  • 大多数类型都能用作函数的返回类型,一种特殊的返回类型是void,表示函数不返回任何值,函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针
  • 形参和函数体内部定义的变量统称为局部变量,仅在函数作用域内可见。所有函数体外定义的对象存在于程序的整个执行过程中,此类对象在程序启动时被创建,直到程序结束才会销毁,而局部变量的生命周期依赖于定义的方式
  • 普通局部变量对应的对象,函数经过变量定义语句时创建该对象,到达定义所在的块末尾时销毁它,称为自动对象。形参是一种自动对象,函数开始时为形参申请存储空间,函数终止时形参被销毁。自动对象如果定义时不含初始值,将执行默认初始化,内置类型的未初始化局部变量将产生未定义的值
  • 将局部变量定义成static类型可以获得生命周期贯穿函数调用及之后时间的对象,称为局部静态对象,局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。如果局部静态变量没有显式的初始值,将执行值初始化,内置类型的局部静态变量初始化为0
  • 函数的名字必须在使用前声明,类似于变量,函数只能定义一次,但可以声明多次,如果一个函数永远也不会被用到,那么它可以只有声明没有定义。函数的声明与定义类似,唯一的区别是函数声明无需函数体,用一个分号代替即可,没有函数体使得函数声明可以省略形参名。函数和变量一样,应该在头文件声明而在源文件定义
  • C++语言支持所谓的分离式编译,允许我们把程序分割到几个文件中,每个文件独立编译,如果修改了其中一个源文件,只需要重新编译改动的文件。大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名为.obj或.o的文件,后缀名表示该文件包含对象代码,接下来编译器负责把对象文件链接在一起形成可执行文件exe

6.2 参数传递

  • 每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。和其他变量一样,形参的类型决定了形参和实参交互的方式,如果形参是引用类型,它将绑定到对应的实参上(引用传递),否则将实参的值拷贝后赋给形参(值传递
  • 可以使用引用形参返回额外信息:一个函数只能返回一个值,如果想用一个函数得到多个结果,可以给函数传入额外的引用形参,函数运行后,返回的值是一个结果,引用形参中的值是另一个结果
  • 和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const,当形参有顶层const时,传给它常量对象或非常量对象都是可以的,函数void f(const int i)和函数void f(int i)本质上是一样的,不能重复定义
  • 形参的初始化方式和变量的初始化方式是一样的,我们可以用非常量初始化一个底层const对象,但反过来不行const int ci = i;,所以函数中有不会改变的形参时要定义成常量引用,否则该形参将不能接受const实参
  • 数组的两个特殊性质对定义和使用作用在数组上的函数有影响,分别是:① 不允许拷贝数组;② 使用数组时通常会将其转换成指针。不能拷贝数组使得无法以值传递的方式使用数组参数,为函数传递一个数组时,实际上传递的是指向数组首元素的指针
  • 把形参写成类似数组的形式有:① void f(const int*);;② void f(const int[]);;③ void f(const int[10]);其中的维度表示期待数组含有多少元素,实际不一定。这三种函数是等价的,编译器处理函数调用时,只检查传入的参数是否是const int*类型,当函数不需要对数组元素执行写操作时,数组形参应该是指向const的指针,需要改变数组元素值时才定义成指向非常量的指针
  • 为了编写能处理不同数量实参的函数,C++11提供了两种主要的方法:① 如果所有实参类型相同,可以传递一个名为initializer_list的标准库类型;② 如果实参的类型不同,可以编写一种特殊的函数,称为可变函数模板。C++还有一种特殊的形参类型省略符,可以传递可变数量的实参,但这种方法一般只用于与C函数交互的接口程序
  • initializer_list是一种标准库类型,用于表示某种特定类型的值的数组,定义在同名的头文件中。initializer_list与vector类似,也是一种模板类型,与vector不一样的是,initializer_list对象中的元素永远是常量值,无法改变void f(initializer_list<int> il)

6.3 返回类型

  • return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方,return语句有两种形式:① return;;② return expression;;,其中无返回值的return语句只能用在返回类型是void的函数中,这类函数的最后一句后面会隐式执行return
  • 只要函数的返回类型不是void,则函数的每条return语句必须返回一个值,且返回值的类型必须与函数的返回类型相同,或能隐式地转换成函数地返回类型
  • 返回一个值的方式和初始化一个变量或形参的方式完全一样,函数返回局部变量时会将返回值拷贝到调用点,如果函数返回引用,则返回的是所引对象的一个别名,不能返回局部对象的引用或指针
  • 调用一个返回引用的函数得到左值,其他返回类型得到右值,可以像使用其他左值那样来使用返回引用的函数调用,也可以为返回类型是非常量引用的函数结果赋值
  • C++11中,函数可以返回花括号包围的值的列表,此处的列表用来对函数返回的临时量进行初始化,如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,且该值所占空间不应该大于目标类型的空间,如果函数返回的是类类型,由类本身定义初始值如何使用
  • 主函数main中如果没有return,编译器会隐式地插入一条返回0的return语句,返回0表示执行成功,返回其他值表示执行失败,其中非0值得具体含义依机器而定
  • 因为数组不能被拷贝,所以函数不能返回数组,但可以返回数组的指针或引用,语法上定义一个返回数组的指针或引用的函数比较繁琐,以下方法可以简化这一任务:① 给数组起别名:using arr = int[10]; arr* f(int i);;② 使用尾置返回类型:auto f(int i) -> int(*)[10];;③ 使用decltype:int arr[]={1,2,3,4,5}; decltype(arr) *f(int i),要注意decltype并不负责把数组类型转换成对应的指针

6.4 函数重载

  • 同一作用域内的几个函数名相同但形参列表不同的为重载函数。顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来,顶层const:void f(int* const);,底层const:void f(const int*),底层const重载函数也可以接受非常量对象,但是会优先选用非常量版本的函数
  • 在C++中,名字查找发生在类型检查之前,如果在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,此时在内层作用域中调用只在外层有的重载函数会报错

6.5 特殊用途语言特性

  • 默认实参:可以为一个或多个形参提供默认值,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值,要使用默认实参,只要在调用函数时省略该实参即可。函数的声明通常放在头文件中,在给定的作用域中一个形参只能被赋予一次默认实参,函数的后续声明只能为之前那些没有默认值的形参添加默认实参,且该形参右侧的所有形参必须都有默认值。局部变量不能作为默认实参,除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参
  • 内联函数:在大多数机器上,一次函数调用包含着一系列的工作,调用前要先保存寄存器,并在返回时恢复,可能要拷贝实参,程序转向一个新的尾置继续执行。将函数指定为内联函数,就是将它在每个调用点上展开,从而消除了函数的运行时开销。在函数的返回类型前加上关键字inline即可将其声明为内联函数,定义在类内部的函数是隐式的inline函数。一般内联机制用于规模较小、流程直接、频繁调用的函数,很多编译器都不支持内联递归函数
  • constexpr函数:指能用于常量表达式的函数,定义constexpr函数的方法与其他函数类似,不过函数的返回类型以及所有形参类型都必须是字面值类型,且函数体中必须有且只有一条return语句,constexpr函数被隐式地指定为内联函数。和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义,但多个定义必须完全一致,于是通常会定义在头文件中
  • 调试帮助:程序可以包含一些用于调试地代码,但是这些代码只在开发程序时使用,当程序编写完成准备发布时,要先屏蔽掉调试代码,这种方法用到两项预处理功能:assertNDEBUG。① assert是一种预处理宏,定义在cassert头文件中,使用一个表达式作为它的条件assert(expr);,首先对expr求值,如果表达式为假(0),assert输出信息并终止程序执行,如果表达式为真(非0)就什么也不做;② NDEBUG预处理变量:assert的行为依赖于一个名为NDEBUG的预处理变量的状态,如果定义了NDEBUG,则assert什么也不做,默认状态下没有定义NDEBUG,此时assert执行运行时检查,可以在main.c文件的开始写入#define NDEBUG来关闭调式状态,也可以通过命令行定义

6.6 函数匹配

  • 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级,具体排序如下:
  1. 精确匹配,包括:① 实参类型与形参类型相同;② 实参从数组类型或函数类型转换成对应的指针类型;③ 向实参添加顶层const或删除顶层const
  2. 通过const转换实现的匹配
  3. 通过类型提升实现的匹配
  4. 通过算术类型转换或指针转换实现的匹配
  5. 通过类类型转换实现的匹配

6.7 函数指针

  • 和其他指针一样,函数指针指向某种特定类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关,想要声明一个指向函数的指针,只需要用指针替换函数名即可bool (*pf)(const string &);,其中指针两端的括号不可少,如果没有括号,则是声明了一个返回值为bool指针的函数
  • 当把函数名作为一个值使用时,该函数自动地转换为指针,pf = func;pf = &func;等价。可以直接使用指向函数的指针调用改函数,无需提前解引用,bool b = pf("hello");bool b = (*pf)("hello");bool b = func("hello");等价。不同函数类型的指针间不能相互转换,可以将函数指针赋值为nullptr或0,表示指针没有指向任何一个函数
  • 与数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,void f(bool pf(const string &));void f(bool (*pf)(const string &));等价,可以直接把函数名作为实参使用,会自动转换成指针。同样,虽然不能返回一个函数,但是能返回指向函数类型的指针,将decltype作用于某个函数时,它返回函数类型而非指针类型

7. 类

7.1 定义抽象数据类型

  • 类的基本思想是数据抽象封装,数据抽象是一种依赖于接口和实现分离的编程技术,封装实现了类的接口和实现的分离,类的用户只能使用接口部分而无法访问实现部分。类想要实现数据抽象和封装,需要首先定义一个抽象数据类型,在抽象数据类型中,类的设计者负责考虑类的实现过程,使用该类的程序员只需要考虑类型做了什么
  • 类本身就是一个作用域,类的成员函数的定义嵌套在类的作用域之内,编译器处理类时会先编译成员的声明,然后才编译成员函数体,因此成员函数体可以随意使用类中其他成员而无须考虑成员的出现次序
  • 默认情况下,this的类型是指向类类型非常量版本的常量指针Person *const,尽管this是隐式的,但它仍然需要遵循初始化规则,意味着默认情况下不能this绑定到一个常量对象上,也使得不能在一个常量对象上调用普通的成员函数。C++允许把const关键字放在成员函数的参数列表之后string func() const;,此时的const表示this是一个指向常量的指针,这样的成员函数被称为常量成员函数,常量对象、常量对象的引用或指针都只能调用常量成员函数
  • 在类的外部定义成员函数:string Person::func() const{}
  • 定义类相关的非成员函数:有些类常常需要定义一些辅助函数,尽管这些函数定义的操作从概念上属于类的接口的组成部分,但它们实际上并不属于类本身,一般这种函数会与类声明在同一个头文件内,用户使用时只需引入一个文件
  • 类通过一个或几个特殊的成员函数来控制其对象初始化的过程,这些函数叫做构造函数,构造函数的任务是初始化类的数据成员,无论何时只要类的对象被创建,就会执行构造函数。构造函数的名字和类名相同,与其他函数不同的是,构造函数没有返回类型,且不能被声明为const的。当创建类的一个const对象时,直到构造函数完成初始化过程,对象才能真正取得其const属性,因此构造函数在const对象的构造过程中可以向其写值(并不是说类的const成员能在构造函数中赋值)
  • 类通过一个特殊的构造函数来控制默认初始化过程,称为默认构造函数,默认构造函数无须任何实参,如果一个类没有显式地定义构造函数,那么编译器就会隐式地定义一个默认构造函数,称为合成的默认构造函数,合成的默认构造函数按照如下规则初始化类的数据成员:如果存在类内的初始值,用它来初始化成员,否则默认初始化该成员。在C++11中,可以指定要求编译器生成默认构造函数Person() = default;
  • 构造函数初始值列表:Person(const string &s):name(s){},构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同,如果不能使用类内初始值,则所有构造函数都应该显式地初始化每个内置类型的成员
  • 除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值、销毁对象时发生的行为。在初始化变量以及以值的方式传递或返回一个对象时会发生拷贝,在使用赋值运算符时会发生赋值,当对象不再存在时发生销毁。若不主动定义这些操作,编译器会自动合成它们,但对于某些类而言,合成的版本无法正常工作

7.2 访问控制与封装

  • struct和class都可以定义类,唯一的区别在于,两者的默认访问权限不一样,struct默认为public,class默认为private
  • 类可以允许其他类或函数访问它的非共有成员,方法是令其他类或函数成为它的友元,如果一个类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可,最好在类定义开始或结束前的位置集中声明友元。友元的声明仅仅指定了访问权限,如果要调用友元函数,必须在友元声明之外再专门对函数进行一次声明,为了使友元对类的用户可见,通常把友元函数的声明与类本身放置在同一个头文件中

7.3 类的其他特性

  • 可以在类中定义某种类型在类内部的别名,该别名也有访问权限
  • 定义在类内部的成员函数是自动inline的,也可以在类外部用inline关键字显式指定成员函数内联,无须再声明和定义处同时说明inline,一般只在类外部定义处说明inline,inline成员函数应该与相应的类定义在同一个头文件中
  • 若希望能修改类的某个数据成员,即使是在一个const成员函数中,可以通过在变量声明处加入mutable关键字做到这一点
  • 类内初始值必须使用符号=的初始化形式,或使用花括号括起来的直接初始化形式
  • 若成员函数返回*this,它是将类对象的引用作为左值返回,若const成员函数返回*this,它返回的类型是常量引用,通过区分成员函数是否是const,可以对其进行重载Person &get(){return *this;} const Person &get() const{return *this;},在某个对象上调用get()时,该对象是否是const决定了该调用哪个版本
  • 每个类定义了唯一的类型,可以把类名作为类型的名字使用,从而直接指向类类型,也可以把类名跟在关键字classstruct后面,Person p;class Person p;两种声明等价
  • 可以像函数一样,仅声明类而暂时不定义它class Person;,这种声明被称为前向声明,对于类型Person来说,在它声明后定义前是一个不完全类型,不完全类型无法创建对象,也无法访问其成员
  • 类除了能将普通的函数定义为友元,还可以把其他类定义成友元friend class Animal,可以把其他类的成员函数定义成友元friend void Animal::eat(int);,如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员,想令某个成员函数作为友元,必须仔细组织程序的结构以满足声明和定义的彼此依赖关系:① 首先定义Animal类,其中声明eat函数,但是不能定义它。在eat使用Person成员之前必须先声明Person;② 接下来定义Person,包括对eat的友元声明;③ 最后定义eat,此时它才可以使用Person的成员
  • 当友元函数定义在类内部时,必须在外部声明一次使得该函数在外部可见
  • 一个类就是一个作用域,在类外部定义类成员时必须同时提供类名和成员名void Person::eat(){}
  • 编译器处理完类中的全部声明后才会处理成员函数的定义
  • 如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化,在构造函数体中对成员进行的是赋值操作。有时候可以忽略初始化和赋值之间的差异,但如果成员是const或引用的话,必须将其初始化,当成员属于某种类类型且该类没有定义默认构造函数时,也必须初始化
  • 成员的初始化顺序取决于它们在类定义中出现的顺序,而非构造函数初始值列表中的前后顺序
  • C++11定义了委托构造函数,一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程Person():Person("",0,0){}
  • 当对象被默认初始化或值初始化时自动执行默认构造函数,默认初始化在以下状态下发生:① 在块作用域内不使用任何初始值定义一个非静态变量或数组时;② 当一个类本身含有类类型的成员且使用合成的默认构造函数时;③ 当类类型的成员没有在构造函数初始值列表中显式地初始化时。值初始化在以下情况下发生:① 在数组初始化地过程中提供地初始值数量少于数组地大小时;② 不适用初始值定义一个局部静态变量时;③ 当使用形如T()的表达式显式地请求初始化时
  • C++在内置类型之间定义了几种自动转换规则,同样也能为类定义隐式转换规则。如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,这种构造函数又称作转换构造函数。编译器只会自动地执行一步类型转换,如果隐式地使用了两种转换规则会报错。将构造函数声明为explicit可以阻止隐式转换,explicit关键字只对一个实参的构造函数有效,且只能在类内声明时使用
  • 聚合类使得用户可以直接访问其成员,且具有特殊地初始化语法形式,当一个类满足以下条件时,是聚合的:① 所有成员都是public的;② 没有定义任何构造函数;③ 没有类内初始值;④ 没有基类,也没有virtual函数struct Person{int age; string name;};,可以使用一个花括号括起来的成员初始值列表来初始化聚合类的数据成员,如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化
  • 有时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联,此时将这种成员声明为静态成员,通过static关键字声明静态成员,该关键字只出现在类内部,在类外部定义静态成员时,不能重复static。由于静态成员不属于类的任何一个对象,所以只能在类的外部定义和初始化静态成员,一个静态数据成员只能定义一次
  • 通常情况下,类的静态成员不能再类内部初始化,但是可以为静态成员提供const整型的类内初始值,但要求静态成员必须是字面值常量类型的constexpr,如果再类内部提供了一个初始值,则再外部定义该成员时就不能再指定初始值了。静态成员可以是不完全类型,且可以用静态成员作为默认实参
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值