后台开发工程师技术能力体系之编程语言1——语法

语法

1. 函数

函数定义:
  函数是一个命名了的代码块,我们通过调用函数执行相应的代码。函数可以有0个或多个参数,通常会产生一个结果。可以重载函数,也就是说,同一个名字可以对应几个不同的函数。
  函数的调用需要完成两项工作:一是实参初始化函数对应的形参;二是将控制权转移给被调用函数,此时,主调函数的执行被暂时中断,被调函数开始执行。当遇到一条return语句时函数结束执行过程,和函数调用一样,return语句也完成两项工作:一是返回return语句中的值(如果有的话),二是将控制权从被调函数转移会主调函数
  实参是形参的初始值,第一个实参初始化第一个形参,第二个实参初始化第二个形参,以此列推。尽管实参和形参存在对应关系,但是并没有规定实参的求值顺序,编译器能以任意可行的顺序对实参求值形参之间不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字
  大多数类型都能作为函数的返回类型,void表示函数不返回任何值。函数的返回类型不能是数组类型或函数类型,但是可以是指向数组或函数的指针
  形参和函数体内部定义的变量统称为局部变量,仅在函数的作用域内可见,在所有函数体之外定义的变量称为全局变量。对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它,我们把这种只存在于块执行期间的局部变量也称为自动对象,例如形参。某些时候,有必要令局部变量的生命周期贯穿函数调用及之后的时间,可以将局部变量定义成static类型,从而获得这样的对象,称为局部静态对象。局部静态对象在程序的执行路径第一次进攻对象定义语句时初始化,并且直到程序终止时才被销毁。如果自动对象不含初始值,将执行默认初始化,而内置类型的未初始化局部变量将产生未定义的值。如果静态局部变量没有初始值,将执行值初始化,内置类型的局部静态变量初始化为0
  和其他名字一样,函数的名字也必须在使用之前声明。类似变量,函数也只能定义一次,但可以声明多次。函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。建议和变量一样,在头文件中进行函数声明而在源文件中进行函数定义,这样就能确保同一函数的所有声明保持一致,而且一旦我们想要改变函数的接口,只需改变一条声明即可。此外,定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配
函数参数:
  每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化,形参只有在函数被调用时才会分配内存单元,在调用结束时,立即释放所分配的内存单元。形参的类型决定了形参和实参交互的方式,如果形参是引用类型,它将绑定到对应的实参上,也就是说,引用形参是它对应的实参的别名,这种方式称为引用传递;否则,将实参的值拷贝后赋给形参,形参和实参是两个相互独立的对象,这种方式称为值传递
  指针形式的形参和其他非引用类型一样,当执行指针拷贝操作时,拷贝的是指针的值,拷贝之后,两个指针是不同的指针,但是由于这两个指针指向了同一对象,所以通过指针形参可以修改它所指向的对象的值,即使这个对象在函数外部。虽然使用指针作为函数形参也能达到与使用引用类似的效果,但是在被调函数中仍然要给指针形参分配存储单元(而引用不需要分配存储单元,起码理论上不用),且需要重复使用"* 指针变量名"的形式,容易出错且阅读性较差;另一方面,在主调函数的调用处,必须有变量的地址作为实参,这些都不太方便。因此在C++中,建议使用引用类型的形参来替代指针。
  值传递涉及到拷贝操作,而拷贝大的类类型对象或容器对象比较低效,甚至有的类类型(例如IO类型)根本不支持拷贝操作,此时只能采用引用传递的方式,避免拷贝操作。同时,如果函数无需改变引用形参的值,最好将其声明为常量引用。此外,一个函数只能返回一个值,如果函数有时需要同时返回多个值,可以通过引用形参为我们一次返回多个结果。
  和其他初始化过程一样,当用实参初始化形参时会忽略掉形参的顶层const,也就是说,形参的顶层const被忽略掉了,当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。忽略掉形参的顶层const可能产生意想不到的结果。

void fcn(const int i) { /*形参为底层const*/}
void fcn(int i){/*错误:重复定义了fcn(int)*/}

  在C++中,允许函数重载,不过前提是不同函数的形参列表应该有明显区别。因为形参顶层const被忽略了,所有上面的代码中传入两个fcn函数的参数可以完全一样,因此第二个fcn函数是重复定义的。
  实际上,形参的初始化方式和变量的初始化方式是一样的,我们可以使用非常量去初始化一个底层const对象,但是反过来不行,也就是说不能用一个常量去初始化一个非底层const对象。同时,一个普通的引用必须用同类型的对象初始化。
  除非需要对形参进行修改,否则我们应该尽可能将形参定位成常量引用。这样即可以避免在函数内部对形参误修改,又能避免限制函数所能接受的实参类型,例如我们不能把const对象、字面值或需要类型转换的对象传递给普通的引用,但却可以传递给常量引用

void fcn1(const string &s){/*s为常量引用*/}
void fcn2(string &s){/*s为普通引用*/}
fcn1("hello world"); //字面值字符串“helloworld”能够传递给常量引用
fcn2("hello world"); //发生编译错误,字面值不能传递给普通引用

  数组特殊的性质对其在函数中的使用也产生了影响:1、不允许拷贝数组,因此无法以值传递的方式使用数组参数;2、使用数组时(通常)会将其转换成指针,因此当向函数传递一个数组时,实际上传递的是指向数组首元素的指针****。尽管不能以值传递的方式传递数组,但是可以把形参写成类似数组的形式。

// 尽管形式不同,但这三个print函数时等价的
void print(const int*);
void print(const int[]);
void print(const int[10]); //这里的维度10表示我们期望数组含有多少元素,实际上并无作用

  main函数是演示C++程序如何向函数传递数组的好例子。main函数通常只有空形参列表:int mian() {......}。然而,有时我们需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数索要执行的操作。例如prog -d -o ofile data0,这些命令行选项通过两个(可选的)形参传递给mian函数:int mian(int argc,char *argv[]){......},第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。因为第二个形参是数组,因此main函数也可以定义为:int main(int argc,char **argv) {......}。当实参传给man函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次指向命令行提供的实参,最后一个指针之后的元素值保证为0。

// 以上面提供的命令行为例,argc应该等于5,argv的内容如下
argv[0] = "prog"; //argv[0]保存的是程序的名字,而非用户输入
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;

  有时我们无法提前预知应该向函数传递几个实参,为了能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型如果实参类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板。initializer_list是一种的标准库类型,用于表示某种特定类型的值的数组,其和vector类似,也是一种模板类型,但是initializer_list对象中的元素永远都是常量值,我们无法修改initializer对象中元素的值

// 用于输出错误信息的函数
void error_msg(initializer_list<string> il)
{
	for(auto beg = il.begin();beg != il.end();++beg)
		cout << *beg << " ";
	cout << endl;
}
// 如果想想initializer_list形参中传递一个值序列,则必须把序列放在一对花括号内
error_msg({"functionC","okay"});
error_msg({"functionC","expected","actual"});

函数返回类型和return语句:
   没有返回值的return语句只能出现在返回类型是void的函数中。返回void的函数不要求非得有return语句,因为void函数的最后一句后面会隐式地执行return。此外,void函数也能使用return语句的第二种形式,但此时return语句的expression必须是另一个返回void的函数。强行令void函数返回其他类型的表达式将产生编译错误。
  函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值

char &get_val(string &str,string::size_type ix)
{
	return str[ix];
}
int main()
{
	string s("a value");
	cout << s << endl; //输出 a value
	get_val(s,0) = 'A'; //将s[0]的值改为A
	cout << s << endl;  //输出 A value
	return 0;
}

  把函数调用放在赋值语句的左侧可能看起来有点奇怪,但其实这没什么特别的。返回值是引用,因此调用是个左值,和其他左值一样,它也能出现在赋值运算符的左侧。但如果返回类型是常量引用,就不能给调用的结果赋值。
  C++11规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时量进行初始化。如果列表为空,临时量执行值初始化,否则,返回的值由函数的返回类型决定。

vector<string> process()
{
	if(expected.empty())
		return {}; //返回一个空vector对象
	else if(expected == actual)
		return {"functionX","okay"}; //返回列表初始化的vector对象
	else
		return {"functionX",expected,actual};
}

  如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。
  main函数的返回值可以看做是转态指示器,返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,我们可以分别使用这两个变量分别表示成功与失败。

int main()
{
	//因为它们是预处理变量,所以既不能在前面加上std::,也不能在using声明中出现。
	if(some_failure)
		return EXIT_FAILURE; 
	else:
		return EXIT_SUCCESS;
}

  如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数。在递归函数中,一定有某条路径不包含递归调用,否则,函数将“永远“递归下去,不断调用它自身直到程序空间耗尽为止。

// 计算val的阶乘,即1*2*3*...*val
int factorial(int val)
{
	if(val > 1) 
		return factorial(val-1)*val;
	return 1;
}

  因为数组不能被拷贝,所以函数不能返回数组,不过,函数可以返回数组的指针或引用。但定义一个返回类型为数组的指针或引用的函数比较繁琐,可以通过一些方法进行简化:最直接的方法是使用类型别名,此外还可以使用尾置返回类型或者decltype关键字。

// 声明一个返回数组指针的函数
int (*p1)[10](int i) {......}
// 1、采用类型别名的方式
typedef int arrT[10]; //等价于using arrT = int[10]
arrT* p2(int i) {......}
// 2、采用尾置放回类型的方式(任何函数都能使用尾置返回,但这种形式对于返回类型比较复杂的函数最有效)
auto p3(int i) -> int(*)[10] {......} //把函数的返回类型放在了形参列表之后,这样就可以清晰的看出函数返回的是一个指向10个整数数组的指针。
// 3、采用decltype关键字的方式(如果我们知道函数返回的指针将指向那个数组,就可以使用decltype关键字声明返回类型)
int arr[10] = {0,1,2,3,4,5,6,7,8,9};
decltype(arr) *p3(int i) {......}

函数重载:
   如果同一作用域内的几个函数名字相同但形参列表不同(形参数量不同或者形参类型不同),我们称之为重载函数。当调用这些函数时,编译器会根据传递的实参类型推断想要的是哪个函数。
   不允许两个函数除了返回类型外其他所有的要素都相同,也就是说,假设有两个函数,它们的形参一样但是返回类型不同,则第二个函数的声明是错误的。此外,由于函数传参时会忽略形参的顶层const,因此无法通过形参是否拥有顶层const来实现函数重载。如果形参是某种类型的指针或引用,则可以通过其是否为底层const来实现函数重载。对于底层const的引用形参(或指针形参),既可以接收常量对象(或常量对象的地址),也能接受非常量对象(或非常量对象的地址),但当我们传递一个非常量对象(或非常量对象的地址时),编译器会优先调用非常量版本的函数

// 不能根据返回类型不同来实现函数重载
int fcn1(int i);
bool fcn1(int i);  // 错误:与上一个函数相比只有返回类型不同
void fcn2(int j);
void fcn2(const int j); //错误:顶层const不能用于实现函数重载
void fcn3(int& ri);
void fcn3(const int& cri); //正确,底层const可用于实现函数重载
void fcn3(int* pi);
void fcn3(const int* cpi); //正确,底层const可用于实现函数重载

   在定义一组重载函数后,编译器首先将调用的实参与重载集合中的每一个函数的形参进行比较,然后根据比较的结果决定调用哪个函数。通常,重载函数之间区别明显,它们要么参数的数量不同,要么参数类型不同,此时确定调用哪个函数比较容易。但在某些情况下,比如当两个重载函数参数数量相同且参数类型可以相互转换时,要想选择调用哪个函数就比较困难,当有多个函数都可以匹配时,编译器选择最佳的一个调用,如果每一个都不是明显的最佳选择,则编译器报错(二义性调用)
   一般来说,将函数声明至于局部作用域内不是一个明智的选择,但是为了说明作用域和重载的相互关系,我们暂时违反这一原则而使用局部函数声明。重载必须基于同一个作用域,在不同作用域中无法重载函数名,也就是说,如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体。

string read();
void print(const string&);
void print(double); //重载print函数
void fooBar(int ival)
{
	// 在C++中,名字查找先于类型检查
	bool read = false; // 新作用域:隐藏了外层的read
	string s = read(); //错误:read是一个布尔值,而非函数
	// 不好的习惯:在局部作用域中声明函数不是一个好的选择
	void print(int); // 新作用域:隐藏了之前的print
	print("value"); //错误:print(const strng&)被隐藏掉了
	print(ival); //正确,当前print(int)可见
	print(3.14); //zhengque,调用print(int);print(double)被隐藏掉了
}

函数模板:
  模板是C++泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者公式。当使用一个vector这样的泛型类型,或者find这样的泛型函数时,我们提供足够的信息,将蓝图转换为特定的类或函数。这种转换发生在编译时。
  当我们需要对不同类型的变量进行同一种操作时,我们可能尝试定义多个重载函数,但如果类型较多,我们则不得不定义多个重载函数,而这些函数的函数体基本一样。

// 假定我们希望编写一个函数来比较两个值,并指出第一个值是小于、等于还是大于第二个值
// 在实际中,我们可能想要定义多个函数,每个函数比较一种给定的类型
int compare(const string &v1,const string &v2)
{
	if(v1 < v2)
		return -1;
	if(v2 < v1)
		return 1;
	return 0;
}
int compare(const double&v1,const double&v2)
{
	if(v1 < v2)
		return -1;
	if(v2 < v1)
		return 1;
	return 0;
}
//这两个函数几乎是相同的,唯一的差异就是参数的类型,函数体则完全一样

  为了避免这种重复定义,我们可以定义一个通用的函数模板,而不是为每个类型都定义一个新函数。一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。模板定义以关键字template开始,后跟一个模板参数列表,同时模板参数列表不能为空。模板参数表示在类或函数定义中用到的类型或值,当使用模板时,我们(隐式或显式地)指定模板实参,将其绑定到模板参数上。例如,我们的compare函数模板声明了一个名为T的类型参数,用名字T表示一个类型,而T表示的实际类型则在编译时根据compare的使用情况来确定。

template <typename T>
int compare(const T &v1,const T &v2)
{
	if(v1 < v2)
		return -1;
	if(v2 < v1)
		return 1;
	return 0;
}

  当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参,即当我们调用compare时,编译器使用实参的类型来确定绑定到模板实参T的类型。例如,在下面的调用中:cout << compare(1,0) << endl; //T为int,实参类型是int。编译器会推断出模板实参为int,并将它绑定到模板参数T。编译器用推断出的模板参数来为我们实例化一个特定版本的函数,当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。

// 实例化出:int compare(const int&,const int&)
cout << compare(1,0) << endl; //T为int
// 实例化出:int compare(const vector<int>&,const vector<int>&)
vector<int> vec1(1,2,3),vec2(4,5,6);
cout << compare(vec1,vec2) << endl; //T为vector<int>

  compare函数有一个模板类型参数T,一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换
  除了定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型,我们通过一个特定的类型名而非关键字class或typename来指定非类型参数当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。

// 定义一个compare函数用于比较字符串字面常量
// 由于不能拷贝数组,所以将函数参数定义为数组的引用
// 希望能比较不同长度的字符串字面常量,因此为模板定义两个非类型的参数
templat<unsigned N,unsigned M> // N表示第一个数组的长度,M表示第二个数组的长度
int compare(const char(&p1)[N],const char(&p2)[M])
{
	return strcmp(p1,p2);
}

  当我们调用这个版本的compare时:compare("hi","mom"),编译器会使用字面常量的大小来代替N和M,从而实例化模板,因此编译器会实例化出如下版本:int compare(const char (&p1)[3],const char(&p2)[4])一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用绑定到整型非类型参数的实参必须是一个常量表达式,绑定到指针或引用非类型参数的实参必须具有静态的生存期,不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用nullptr或一个值为0的常量表达式来实例化。
  函数模板可以声明为inline或constexpr,同非模板参数一样,inline或constexpr说明符放在模板参数列表之后,返回类型之前。

template<typename T> inline T min(const T&,const T&); //正确
inline template<typename T> T min(const T&,const T&); //错误

  当编译器遇到一个模板定义时,它并不生成代码,只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。这个特性影响了我们如何组织代码以及错误何时被检测到。通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义,即函数模板和列模板成员函数的定义通常放在头文件中
  在编写函数模板时,可以先写一个函数,然后把其中需要变化的变量类型都替换成虚拟类型即可。可以看到,用函数模板比函数重载更加方便,但是它只适用于参数个数相同但类型不同的情况。

2. 数组

数组的定义:
  数组是一种类似标准库类型vector的数据结构。与vector相似的地方是,数组也是存放类型相同的对象的容器,这些对象本身没有名字,需要通过其所在位置访问。与vector不同的地方是,数组的大小确定不变,不能随意向数组中增加元素。数组的声明形如type a[d],其中a是数组的名字,d是数组的维度,表示数组中元素的个数,因此必须大于0。数组的维度也属于数组类型的一部分(数组类型为type[d]),编译时维度应该是已知的,因此维度必须是第一个常量表达式
  默认情况下,数组的元素被默认初始化,和内置类型的变量一样,如果在函数内部定义了某种内置类型的数组,那么默认初始化会令数组含有未定义的值。定义数组的时候必须指定数组的类型,不允许使用auto关键字由初始值的列表推断类型。不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
字符数组:
  字符数组有一种额外的初始化形式:可以用字符串字面值对字符数组初始化,但一定要注意,字符串字面值的结尾处还有一个空字符,这个空字符也会被拷贝到字符数组中去。

char a1[] = "C++"; // a1的大小为4而不是3,因为字符串字面值的末尾还有一个空字符
char a2[5] = "hello"; //错误:没有空间存空字符

  对于字符数组而言,,比较容易混淆的是sizeof()strlen()
  1、sizeof()是运算符,不是函数,在编译时就计算好了,用于计算数据空间的字节数。因此,sizeof常用于计算内置类型和静态分配的对象、结构或数组所占的空间,不能用来返回动态分配的内存空间的大小。当参数为数组时,sizeof返回的是编译时分配的数组空间大小(数组名用于sizeof时不会自动转换成指针),例如char a[10] = "hello";sizeof(a)返回的是数组空间大小10,而不是C风格字符串占据的空间大小6;当参数是指针时,sizeof返回的实际存储该指针所用的空间大小4,例如char *str = "hello";sizeof(str)返回的是指针大小4,而不是其所指向的C风格字符串所占的空间大小6;当参数是内置类型时,返回的是该类型所占的空间大小,例如int b = 10;,因为在32位机器上,int类型占4Byte,所以sizeof(b)的值是4;当参数是类类型时,返回的是对象实际占用的空间大小,例如class Sample { int a,b; int fun(); } sample;,两个int类型的值是8Byte,所以sizeof(sample)的值是8;当参数是函数时,返回的是函数的返回类型所占的空间大小,且函数的返回类型不能是void。
  2、strlen()是函数,在运行时才能计算。参数必须是字符型指针(char *),且必须是‘\0’结尾的,即C风格字符串。当数组名作为参数传入strlen()时,实际上数组已经退化成指针了。它的功能是返回字符串的长度,从代表该字符串的第一个地址开始遍历,直到遇到结束符'\0',返回的长度不包括‘\0’

3. 指针

指针的概念:
  指针是“指向”另外一种类型的复合类型,与引用类似,指针也实现了对其他对象的间接访问。然而指针与引用相比又有很多不同点:1、指针本身就是一个对象,允许对指针赋值和拷贝;2、指针无须在定义时赋值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
  定义指针类型的方法是将声明符写成type *d的形式,其中d是变量名。如果在一条语句中定义了几个指针变量,每个变量名前面都必须有符号*。指针的值应属于下列4种转态之一:1、指向一个对象;2、指向紧邻对象所占空间的下一位置;3、空指针,意味着指针没有指向任何对象;4、无效指针,也就是上述情况之外的其他值。尽管第2种和第3种形式的指针是有效的,但其使用同样受到限制,因为这两种形式的指针没有指向任何具体对象,所以试图访问此类指针对象的行为不被允许,如果这样做了,后果无法预计。
  空指针不指向任何对象,在试图使用一个指针之前,代码应该首先检查它是否为空。得到空指针最直接的办法就是用字面值nullptr来初始化指针,这是C++11新引入的一种方法。nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。另外还可以通过将指针初始化为字面值0或使用预处理变量NULL(预处理变量,在头文件cstdlib中定义,它的值实际上就是0)来生成空指针,但不能将值为0的int变量赋给指针。
  void*是一种特殊的指针类型,可用于存放任意类型对象的地址。和普通指针类似的是,一个void*指针存放着一个地址;但不同的是,我们对该指针中到底是个声明类型的对象并不了解。因此利用void*指针能做的事也很有限:和别的指针比较大小(按存储的地址进行比较,但必须是同一类型的指针)、作为函数的输入输出、赋给另外一个void*指针,但不能直接操作void*指针所指的对象,必须进行将其转换为实际类型的指针之后才能操作。
数组与指针:
  在C++语言中,数组和指针有着十分紧密的联系:在很多用到数组名字的地方,编译器都会自动将其替换为一个指向数组首元素的指针

int nums[] = {1,2,3};
int *p1 = nums; //等价于p1 = &nums[0]
auto p2(nums); //p2是一个int指针,指向nums的第一个元素
decltype(nums) ia = {0,1,2,3,4,5}; //当使用decltypa时,数组不会自动转换为指针

  当程序使用多维数组的名字时,也会自动将其转换成指向数组首元素的指针。因为多维数组实际上是数组的数组,所以由多维数组名转换得来的指针实际上是指向第一个内层数组的指针

int ia[3][4]; //大小为3的数组,每个元素是含有4个整数的数组
int (*p)[4] = ia; //p指向含有4个整数的数组
p = &ia[2]; //p指向ia的尾元素

函数与指针:
  函数指针指向的是函数而非对象。和其他指针一样,函数指针指向某种特定函数类型,函数的类型由它的返回类型和形参类型共同决定,与函数名无关。要想声明一个可以指向某个函数的指针,只需要将对应函数声明中的函数名用指针替代即可。

bool lengthCompare(const string&,const string&); //声明一个函数
bool (*pf)(const string&,const string&); //声明一个函数指针
 //pf两端的括号必不可少
bool *pf(const string&,const string&); // 如果不写这对括号,pf是一个返回值为boll指针的函数

  当把函数名作为一个值使用时,该函数自动地转换成指针。此外,我们还能直接使用指向函数的指针来调用该函数,无须提前解引用指针。在指向不同函数类型的指针之间不存在转换规则,但和其他指针一样,我们可以为函数指针赋一个nullptr或者值为0的整型常量表达式,表示该值没有指向任何一个函数

pf = lengthCompare; //pf指向名为lengthCompare的函数
pf = &lengthCompare; //等价的赋值语句,取地址符是可选的

bool b1 = pf("hello","bye"); //通过函数指针调用lengthCompare函数
bool b2 = (*pf)("hello","bye"); // 等价调用
bool b3 = lengthCompare("hello","bye"); // 另一个等价调用

int sumLength(const string&,const string&);
bool cstringCompare(const char*,const char*);
pf = nullptr; //正确,pf不指向任何函数
pf = sumLength; //错误,返回类型不匹配
pf = cstringCompare; //错误,形参类型不匹配

  当我们使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数,如果定义了指向重载函数的指针,则编译器通过指针类型决定选用哪个函数,指针类型必须与重载函数中的某一个精确匹配,否则发生编译错误

// 重载函数
void ff(int*);
void ff(unsigned int);

void (*pf1)(unsigned int) = ff; //pf1指向ff(unsigned int)
void (*pf2)(int) = ff; //错误,没有任何一个ff该与该形参列表匹配
double (*pf3)(int *) = ff; //错误ff和pf3的返回类型不匹配

  和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,我们可以直接将形参定义成函数的格式,还可以直接把函数作为实参使用,它们都会自动转换成指针。如果直接使用函数指针类型显得很繁琐,可以采用类型别名或者decltype关键字进行简化,需要注意,decltype不会将函数类型自动转换成指针类型,需要程序员手动添加*。

// 第三个形参是函数类型,它会自动转换成指向函数的指针
void useBigger(cont string &s1,const string &s2,bool pf(const string &,const string &));
// 等价声明:显式地将形参定义成指向函数的指针
void useBigger(cont string &s1,const string &s2,bool (*pf)(const string &,const string &));
// 自动将实参函数lengthCompare转换成指向该函数的指针
useBigger(s1,s2,lengthCompare);

// 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; //等价的类型
// useBigger的等价声明,其中使用了类型别名
void useBigger(const string&,const string&,Func);
void useBigger(const string&,const string&,FuncP2);
// 这两个语句声明的是同一个函数,在第一条语句中,编译器自动地将Func表示的函数类型转换成指针

  和数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。然而,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。

// 使用类型别名进行简化
using F = int(int*,int); //F是函数类型,不是指针
using PF = int(*)(int*,int); // PF是指针类型

PF f1(int); //正确,PF是指向函数的指针,f1返回指向函数的指针
F f1(int); //错误:F是函数类型,f1不能返回一个函数
F *f1(int); //正确,显式地指定返回类型是直线函数的指针
//尾置返回类型
auto f1(int) -> int(*)(int*,int);

4. 引用

  引用为对象起了另外一个名字(引用即别名),引用类型引用另外一种类型。通过将声明符写成type &d的形式来定义引用类型,其中d是声明的变量名。一般在初始化变量时,初始值会被拷贝到新建的对象中,然而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行。
  允许在一条语句中定义多个引用,但是每个引用标识符都必须以符号&开头。除了两种特殊的情况外,其他所有引用的类型都要和绑定的对象严格匹配。此外,(左值)引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起

int i1 = 1024,i2 = 2048;
int &r1 = i1,r2 = i2; // r1是一个引用,与i1绑定在一起;而r2是一个int
int &r3 = i1,&r4 = i2; // r3和r4都是引用
int &ri = 10; //错误,引用类型的初始值不能是字面值,必须是一个对象
double j = 3.14;
int &rj = j; //错误,此处的引用类型的初始值必须是int型对象

5. 联合、枚举

联合类型:
  联合(union)是一种特殊的类,一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当我们给union的某个成员赋值之后,该union的其他成员就变成未定义的转态。分配给一个union对象的存储空间至少要能容纳它的最大的数据成员。和其他类一样,一个union定义了一种新类型。
  类的某些特征对union同样适用,但是并非所有特征都如此。union不能含有引用类型的成员,除此之外,它的成员可以是绝大多数类型在C++11新标准中,含有构造函数或析构函数的类类型也可以作为union的成员类型。union可以为其成员指定public、protected和private等保护标记。默认情况下,union的成员是公有的,这一点与struct相同。
  union可以定义包括构造函数和析构函数在内的成员函数,但是由于union既不能继承自其它类,也不能作为基类使用,所以在union中不能含有虚函数
  union提供了一种有效的途径使得我们可以方便地表示一组类型不同的互斥值。假设我们需要处理一些不同种类的数字数据和字符数据,则可以定义一个union来保存这些值:

// Token类型的对象只有一个成员,该成员的类型可能是下列类型中的任意一种
union Token
{
	char vcal;
	int ival;
	double dval;
};

  union的名字是一个类型名,和其他内置类型一样,默认情况下union是未初始化的。如果提供了初始值,则该初始值被用于初始化第一个成员。为union的一个数据成员赋值会令其他数据成员变成未定义的状态,因此当我们使用union时,必须清楚知道当前存储在union中的值到底是什么类型。

Token fisr_token = {'a'}; //用花括号的形式显式初始化一个union,实际上初始化的是第一个成员cval
Token last_token; //未初始化的Token对象
last_token.ival = 1; //通过成员访问运算符访问union的数据成员并赋值
Token *pt = new Token; //指向一个未初始化的Token对象的指针
pt->dval = 3.14; //通过指针对数据成员进行赋值

  匿名union是一个未命名的union,一旦我们定义了一个匿名union,编译器就自动为该union创建一个未命名的对象。在匿名union的定义所在的作用域内该union的成员都是可以直接访问的。此外,匿名union不能包含受保护的成员或私有成员,也不能定义成员函数。

union {
	char cval;
	int ival;
	double dval;
}; // 定义一个未命名的对象,我们可以直接访问它的成员
cval = 'c'; //为刚刚定义的为命名的匿名union对象赋一个新值
ival = 42; //该对象当前保存的值是42

  可以使用union判断系统是big endian(大端)还是little endian(小端)。大端是指低地址存放最高有效字节,有利于判断数值的正负,常用于网络通信;小端是指低地址存放最低有效字节,有利于进行四则运算,常用于主机存储。因此,当两台主机进行通信是,在发送数据之前都必须将主机字节序(小端)转换转换成网络字节序(大端)。例如,如果将0x1234abcd写入0x0000开始的内存中,则结果如下所示:
大小端内存地址增长方向
  由于union存放顺序是所有成员都是从低地址开始,因此可以通过union判断系统是大端还是小端:

bool isBigEndian()
{
    union endian
    {
       int i;
       char c;
    };

    endian u;
    u.i= 0x12345678; // 2个16进制数表示1个字节
    
    if (u.c == 0x12)     return true;  // 如果低地址存放的是高位字节,则为大端,否则为小端
    return false;
}

  一般64机器上各个数据类型所占的存储空间如下:char(1byte),short(2byte),int(4byte),long(8byte),float(4byte),double(8byte),long long(8byte),其中long类型在32位机器上只占4byte,其他类型在32位机器和64位机器都是占同样的大小空间。下面,我们以64位机器为例,分别介绍结构体和联合类型占用的内存大小:1、union A { int a[5]; char b;double c; };,union中的变量共用内存,应以最长的为准,但是执行sizeof()运算符得到的结果不是预想的20(int a[5],5*4=20byte),这是因为union体内变量的默认内存对其方式必须以最长的类型对齐(这里最长的类型是double,8byte),因此最终的结果应该是24byte(8的整数倍,且最接近20)。2、struct B{ char a; double b; int c;};,执行sizeof()运算符之后的结果不是预想的13(1+8+4=13byte),这是因为结构体的变量需要按照不同的类型进行字节对齐。char a的偏移量是0,占用1byte;bouble b的偏移量是8,当前指向的可用地址为1,需要补足7byte;int c的偏移量是4,当前指向的可用地址为16,是4的倍数,满足int的对齐方式。当所有成员变量都满足对齐方式之后,总空间的大小为1+7+8+4=20byte,不是结构的字节边界数(即结构体中最大类型所占字节数的整数倍,这里是double所占字节数8byte),所以还需要再填充4byte,最终的总空间大小为24。

枚举类型:
  枚举类型使我们可以将一组整型常量组织在一起。和类一样,每个枚举类型定义了一种新的类型。枚举属于字面值常量类型。C++包含两种枚举:限定作用域的和不限定作用域的。C++新标准引入了限定作用域的枚举类型,定义限定作用域的枚举类型时使用的关键字是enum class(或者等价地使用enum struct):enum class open_mode {input,output,append};。定义不限定作用域的枚举类型时使用的关键字是enum(省略掉关键字class或struct),枚举类型的名字时可选的:enum color {red,yellow,green}; enum {floatPrec = 6,doublePrec = 10, doublePrec = 10};,如果enum是未命名的,则我们只能在定义该enum时定义它的对象。
  在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的。与之相反,在不限定作用域的枚举类型中,枚举成员的作用域与枚举类型本身的作用域相同

enum color {red,yellow,green}; //不限定作用域的枚举类型
enum stoplight {red,yellow,green}; //错误,重复定义了枚举成员
enum class peppers {red,yellow,green}; //正确,外层作用域的枚举成员被隐藏了

color eyes = green; //正确,不限作用域的枚举类型的枚举成员位于有效的作用域中
peppers p1 = green; //错误,peppers的枚举成员不在有效的作用域中,这里的green是指color::green,类型不匹配
peppers p2 = peppers::red; //正确,使用peppers的red

  默认情况下,枚举值从0开始,依次加1。不过我们也能为一个或多个枚举成员指定专门的值,且枚举值不一定唯一。如果没有显式地提供初始值,则当前枚举成员的值等于之前枚举成员的值加1:

enum class intTypes{
charTyp = 8,shortTyp = 16,intTyp = 16,
longTyp = 32,long_longTyp = 64
};

  枚举成员是const,因此在初始化枚举成员时提供的初始值必须是常量表达式,也就是说,每个枚举成员本身就是一条常量表达式,我们可以在任何需要常量表达式的地方使用枚举成员。例如,我们可以定义枚举类型的constexpr变量:constexpr intType charbits = intType::charTyp;;类似的,我们也可以将一个enum作为switch语句的表达式,而将枚举值作为case标签
  和类一样,枚举也定义了新的类型,只要enum有名字,我们就能定义并初始化该类型的成员。要想初始化enum对象或者为enum对象赋值,必须使用该类型的一个枚举成员或者该类型的另一个对象,而不能使用整型数值一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型,因此我们可以在任何需要整型值的地方使用它们,但限定作用域的枚举类型不能自动转换成整型

open_modes om = 2; //错误:2不属于类型open_modes
om = open_modes::input; //正确:input是open_modes的一个枚举成员
int i = color::red; //正确:不限定作用域类型的枚举成员隐式地转换成int
int j = peppers::red; //错误:限定作用域的枚举类型不会进行隐式转换

  尽管每个enum都定义了唯一的类型,但实际上enum是由某种整数类型表示的。在C++11新标准中,我们可以在enum的名字后加上冒号以及我们想要在该enum中使用的类型:enum intValues : unsigned long long { charTyp = 255,shortTyp = 65535,intTyp = 65535,longTyp = 4294967295UL,long_longTyp = 18446744073709551615ULL};。如果我们没有指定enum的潜在类型,则默认情况下限定作用域的enum成员类型是int对于不限定作用域的枚举类型来说,其枚举成员不存在默认类型,我们只知道成员的潜在类型足够大(不同机器的潜在类型可能不同),肯定能够容纳枚举值。我们我们指定了枚举类型的潜在类型,一旦某个枚举成员的值超出了该类型所能容纳的范围,将引发程序错误。
  在C++11新标准中,我们可以提前声明enum,同时enum的前置声明必须指定其成员类型,对于限定作用域的enum来说,我们可以不指定其成员类型,这个值被隐式地定义成int;而不限定作用域的enum来说,必须显式指定其成员类型。

enum intValues : unsigend long long; // 不限定作用域的,必须指定成员类型
enum class open_modes; //限定作用域的可以使用默认成员类型int

  对于enum形参而言,必须传递该enum类型的实参,不能直接将整型值传递给enum形参。

enum Tokens {INLINE = 128,VIRTUAL = 129}; //枚举类型
void ff(Tokens); //函数声明
void ff(int); //函数声明
int main(){
	Tokens curTok = INLINE;
	ff(128); //精确匹配ff(int)
	ff(INLINE); //精确匹配ff(Tokens)
	ff(curTok); //精确匹配ff(Tokens)
	return 0;
}

  但是反过来可以,我们可以将一个不限定作用域的枚举类型的对象或枚举成员传递给整型形参,此时enum的值提升成int或更大的整型,即使enum的潜在类型或显式指定的类型比int范围小,也会被提升到int。

void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;
newf(VIRTUAL); // 调用newf(int)
newf(uc); // 调用newf(unsigned char)

6. 预处理

头文件保护符
  确保头文件多次包含仍能安全工作的常用技术是预处理器,它有C++语言从C语言继承而来,预处理器是在编译之前执行的一段程序,可以部分地改变我们所写的程序。之前已经用到了一项预处理功能#include当预处理器看到#include标记时,就会用指定的头文件内容代替#include
  C++程序还会用到的一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define指令把一个名字设定为预处理变量,另外两个指令分别检查某个指定的预处理变量是否已经定义:#ifdef当且仅当变量已定义时为真,#ifndef当且仅当变量未定义时为真。一旦检查结果为真,则执行后续操作直至遇到#endif指令为止。使用这些功能能够有效地防止重复包含的发生:

#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
struct Slaes_data {
	std::string bookNo;
	unsigned units_sold = 0;
	double revenue = 0.0;
};
#endif

  第一次包含Sales_data.h时,#ifndef的检查结果为真,预处理器将顺序执行后面的操作,直到遇到#endif为止。此时,预处理变量SALES_DATA_H的值将变为已定义,而且Sales_data.h也会被拷贝到我们的程序中来。后面如果再一次包含Sales_data.h,则#ifndef的检查结果将为假,编译器将忽略#ifndef到#endif之间的部分。预处理变量无视C++语言中关于作用域的规则,在整个程序范围内都有效。整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性。为了避免与程序中的其他实体发生名字冲突,一般把预处理变量的名字全部大写。
链接指示:extern "C"
  C++有时需要调用其他语言编写的函数,最常见的是调用C语言编写的函数。像所有其他名字一样,其他语言中的函数名字也必须在C++中进行声明,并且该声明必须指定返回类型和形参列表。对于其他语言编写的函数来说,编译器检查其调用的方式与处理普通函数的方式相同,但生成的代码有所区别。C++使用链接指示指出任意非C++函数所用的语言。
  要想把C++代码和其他语言(包括C语言)编写的代码放在一起使用,要求我们必须有权访问该语言的编译器,并且这个编译器与当前的C++编译器是兼容的
  声明一个非C++的函数,链接指示有两种形式:单个的或复合的。链接指示不能出现在类定义或函数定义的内部同样的链接指示必须在函数的每个声明中都出现

// 可能在C++头文件<cstring>中的链接指示
// 单语句链接指示
extern "C" size_t strlen(const char *);
// 复合语句链接指示
extern "C"
{
	int strcmp(const char*,const char*);
	char *strcat(char*,const char*);
}

  链接指示的第一种形式包含一个关键字extern,后面是一个字符串字面值常量以及一个“普通的”函数声明,其中的字符串字面值常量指出了编写函数所用的语言。编译器应该支持对C语言的链接指示,此外编译器也可能支持其他语言的链接指示,例如extern "Ada”、extern "FORTRAN"
  我们也可以令链接指示后面跟上花括号括起来的若干函数的声明,从而一次性建立多个链接。花括号的作用是将适用于该链接指示的多个声明聚合在一起。花括号中声明的函数名字时可见的,就好像在花括号之外声明的一样。
  多重声明的形式还可以应用于整个头文件,当一个#include指示被放置在复合链接指示的花括号中时,头文件中的所有普通函数声明都被认为是由链接指示的语言编写的链接指示可以嵌套,因此如果头文件包含带自带链接指示的函数,则该函数的链接不受影响

// C++的cstrin头文件可能形如:
extern "C"
{
#include <string.h> //操作C风格字符串的C函数
}

  编写函数所用的语言是函数类型的一部分,因此,对于使用链接指示定义的函数来说,它的每个声明都必须使用相同的链接指示。而且,指向其他语言编写的函数的指针必须与函数本身使用相同的链接指示:extern "C" void (*pf) (int);,pf指向一个C函数,该函数接收一个int并返回void。当我们使用pf调用函数时,编译器认定当前调用的是一个C函数。指向C函数的指针与指向C++函数的指针是不一样的类型一个指向C函数的指针不能用在执行初始化或赋值操作后再指向C++函数,反之亦然。就像其他类型不匹配的问题一样,如果我们试图在两个链接指示不同的指针之间进行赋值操作,则程序将发生错误。

void (*pf1) (int); //指向一个C++函数
extern "C" void (*pf2) (int); //指向一个C函数
pf1 = pf2; //错误,pf1和pf2类型不同

  链接指示对整个声明都有效。当我们使用链接指示时,它不仅对函数有效,而且对作为返回类型或形参类型的函数指针也有效extern "C" void f1(void(*) (int));。f1是一个C函数,它的形参是一个指向C函数的指针,当我们调用f1时,必须传给它一个C函数的名字或指向C函数的指针。因为链接指示同时作用于声明语句中的所有函数,所以如果我们希望给C++函数传入一个指向C函数的指针,则必须使用类型别名

// FC是一个指向C函数的指针
extern "C" typedef void FC(int);
// f2是一个C++函数,该函数的形参是指向C函数的指针
void f2(FC *);

  导出C++函数到其他语言,通过使用链接指示对函数进行定义,我们可以令一个C++函数在其他语言编写的程序中可用:extern "C" double calc(double dparm) {/*......*/},编译器将为该函数生成适合于指定语言的代码。值得注意的是,可被多种语言共享的函数的返回类型或形参类型受到很多限制。例如,我们不太可能把一个C++类的对象传给C程序,因为C程序根本无法理解构造函数、析构函数以及其他类特有的操作。
  有时需要在C和C++中编译同一个源文件,为了实现这一目的,在编译C++版本的程序时预处理器会定义__cplusplus,利用这个变量,我们可以在编译C++程序的时候有条件地包含进来一些代码:

#ifdef __clpusclpus //正确:正在编译C++程序
extern "C" {
#endif
/*......*/
#ifdef __clpusclpus
}
#endif

  链接指示与重载函数的相互作用依赖于目标语言,如果目标语言支持重载函数,则为该语言实现链接指示的编译器很可能也支持重载这些C++的函数。C语言不支持函数重载,因此也就不难理解为什么一个C链接指示只能用于说明一组重载函数中的某一个了:

// 错误:两个extern"C"函数的名字相同
extern "C" void print(const char*); 
extern "C" void print(int);
// 如果一组重载函数中有一个是C函数,则其余的必定都是C++函数
class SmallInt { /*......*/};
class BigNum { /*....*/};
// C函数可以在C或C++程序中调用
// C++函数重载了该函数,可以在C++程序中调用
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
extern BigNum calc(const BigNUm&);
// C版本的calc函数可以在C或C++程序中调用,而使用了类类型形参的C++函数只能在C++程序中调用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值