本小节主要讲解一些以下C++的基础概念:1.命名空间 2.输入输出 3.缺省参数 4.函数重载 5.引用 6.内联函数 7.auto关键字 8.基于范围的for循环 9.空指针nullptr
命名空间定义命名空间,需要用到namespace关键字。在说到命名空间前我们可以先来看看下面这个小例子。
可以看到,我定义了一个变量malloc,并打印了这个变量。这是没有任何问题,完全可以的。但是当我包含了stdlib库时,这个代码就要报错了。
显然这是malloc变量和malloc函数重名导致的。那么如果在某一个工程中,我和别的代码编辑者都定义了一个x,只不过我写的是函数,他写的是变量。这导致最后起了冲突,需要修改,但是代码量比较大,一个一个去修改太麻烦了。那么C++就给了一种方法,这种方法就是命名空间。简单地讲,命名空间,就是给这些变量名一个空间,与外界隔离开,这样我和他的x就分属不同的命名空间。
namespace My
{
void x()
{
printf("My namespacen");
}
};
namespace His
{
int x = 10;
};
那么现在隔离开了,怎么取用呢?使用::。如这段代码。
int main()
{
My::x();
printf("%dn",His::x);
return 0;
}
命名空间还有这样一种用法,用using,达到全展开,就像引用库一样。引用库的时候编译器把库里的代码全部复制到代码区内。但是这样的话,我们为什么还要发明命名空间呢?所以一般都是单独只从中展开某个函数或变量。
using namespace My; // 全展开 using My::x; // 单独展开x输入与输出
C++的输入输出需要用到流 << , >>这个运算符。
当需要输入时,则从标准输入流(键盘) 流入 空间。
cin >> x;
当需要输出时,则从空间 流入 标准输出流(屏幕)。
cout << x;
很形象对吧。而且C++支持连续流入,如。
cout << x << y;
然后是换行,可以使用C中的n,也可以用C++的endl。
注意,cin,cout,endl,都需要引用iostream库,和使用std命名空间。如果只引用iostream库,那么需要在cin,cout,endl前加上std::。这就是前面讲到的命名空间。
其实C++的输出流是比较好用的,它和python一样,不需要像C一样关心输出的数据类型。这不过这也导致C更好格式化输出。
缺省参数你有这样一个函数Print,在默认情况下,它应该输出123。即,当用户不传入参数时,函数会有一个默认参数。C做不到,但是C++可以做到。这就是缺省参数,即在定义一个参数的时候就一个给了一个默认值。
如下面这个函数。
void Add(int x = 1, int y = 2)
{
cout << x + y << endl;
}
int main()
{
Add();
Add(3);
Add(3, 4);
return 0;
}
缺省参数有几个需要注意的地方:
1.缺省参数应该从右到左缺省,比如有5个参数,当第一个缺省的参数为第三个时,那么它之后的第四个,第五个参数都应该缺省。为什么呢?拿这个Add举例子。当我只让第一个参数缺省,且我只传一个参数进去,这该如何是好,是把这个变量传给缺省参数,还是后面一个普通参数。这就矛盾了。因为既然我只传一个参数,那么我就是想让第一个参数为这个数,但是现在就很矛盾了。再举个例子,5个参数,前3个为缺省,传3个参数,怎么办。这种就搞不清楚你到底要传给谁了。这种模糊不清违反了精准的原则。
2.含有缺省参数的函数的缺省参数只能出现在声明里。什么意思呢。就是当我们把缺省函数的声明和定义分离的时候,在只能在声明中写它具有缺省参数。当声明和定义中都写了缺省参数时,缺省参数会重复。当只有定义中有缺省参数时,由于编辑器是根据声明来函数使用是否合法的。这就导致编译错误。
函数重载在C语言中,写一个Add函数是很困难的。因为C语言的函数名字是唯一的。这样我们就要为两个整数写一个IntAdd,为两个浮点数写一个FloatAdd,而且在调用这些函数时,如果不能提前知道,要相加的两个参数的数据类型,是很麻烦的。C++依靠函数重载解决了这个问题。
什么是函数重载呢,也就是允许相同函数名称的不同参数类型的函数存在。如这段代码。
void Add(int x, int y)
{
cout << x + y << endl;
}
void Add(float x, float y)
{
cout << x + y << endl;
}
void Add(float x, int y)
{
cout << x + y << endl;
}
void Add(int x, float y)
{
cout << x + y << endl;
}
他们都是Add函数,当你传入两个参数时,编辑器会根据传入参数的类型,去调用对应的函数。这就是函数重载。注意,重载的函数必须名字相同,参数不同。参数不同,不是说参数的名字不同,而是对于的类型位置不同。如 int float 和 float int就是不同的。
那么C++是如何实现函数重载呢?这里要考虑到程序是如何生成的。我们知道一段经过预处理的C/C++代码是经过编译和链接两大步生产可执行程序的。那么在链接的过程中会产生一张符号表,里面有函数的名称(经过处理后的名称,不是函数名)和函数的地址。那么为什么C++能支持重载呢?
这里我写了4个重载函数。
void Add(int x, int y)
{
cout << x + y << endl;
}
void Add(float x, float y)
{
cout << x + y << endl;
}
void Add(float x, int y)
{
cout << x + y << endl;
}
void Add(int x, float y)
{
cout << x + y << endl;
}
int main()
{
int x = 1;
float y = 1.25;
Add(x, y);
Add(y, x);
return 0;
}
我们来看看他的反汇编。
我们可以看到call调的并不是同一个地址。其实更深层的原因在于C++对于函数的命名规则和C是不一样的。C只将函数名作为函数的命名,而对于C++,函数的命名不仅包括了函数名,还包括了参数的类型,所以对于C++ 来讲,他们并不是同一个函数。而C则认为是同一个函数,于是起冲突了。
引用引用是C++一个很神奇的用法,用到&关键字,但是他并不是取地址,而是取别名。就如我们说二也说两是一个意思。
注意,引用在定义时必须初始化,一个变量可以有多个应用。
int x = 0; int& z = x; // 必须初始化 int& y; // 这样写是不行的
既然引用是起别名,那么当我们修改引用时,其实也就是修改了原来的值。这可以用到函数参数中,这样就可以不传指针了,也更形象。然后也可以作为返回值。那么如果我们使用引用返回,是将函数内创建的变量返回,是很危险的,这类似于野指针。因为这个引用所代表的空间已经被销毁了,这时去访问它,将会是随机值。如下面代码。
int& Add(int a, int b)
{
int c = a + b;
return c;
}
那么引用返回有什么用呢,其实是很有用的,虽然我们不能将新创建的变量引用返回,但是如果我们返回的是一个不会被函数范围锁限制的变量,也就是出了函数不会被销毁的变量,引用返回的效率很高。正常函数在返回非引用值的时候,如果存储这个值的变量出了函数要销毁,那么函数会先生成一个新的只读的变量来存储返回值,然后销毁函数内变量,接着返回这个只读变量。如果返回的是引用值,则会跳过生产中间只读变量环节,直接返回。
引用的权限问题,其实这个问题和指针是一样的。当我某个函数的参数是int& a时,其实代表这个a是可以被修改的。如果此时我传入一个const int&类型的变量,这其实是一种权限的放大,vs会报错。所以我们在处理不需要修改的引用参数时,最好都加const。这个问题尤其和函数是如何返回值会擦出奇妙火花。
int& Add(const int& a)
{
return a;
}
这个代码就涉及到了权限放大,返回是可以修改的,但是a是有const修饰的。
还有这样的。
void func1(int* const tmp)
{
cout << *tmp << endl;
}
void func2(const int& a)
{
func1(a);
}
func1中的tmp是一个指针,const修饰它的空间地址不会被修改,但是*tmp可以修改。func2里传入的是引用,const修饰值不能被修改。此时传入,权限就得到放大。
内联函数内联函数的关键字为,inline。内联函数和宏是一样的,他会把代码直接展开到调用的位置,对于少量的代码,如10行以下,而且使用频率高的函数,内联就不需要一直不停地压栈出栈等行为,提高了程序效率。那么既然有宏为什么要有内联呢?
原因是宏很容易写错,比如一个加法宏。正确的写法应是这样。
#define ADD(a, b) ((a) + (b))
括号是一个都不能少的,因为宏真是非常粗暴的替换。如果优先级发生错乱是很危险的。但是函数不一样,函数我们很难写错的。所以内联是有它的意义的。
auto关键字auto有点像python里的eval。是一个自动识别的类型。编译器在编译的时候会自动推导该变量的类型。注意,auto不能作为函数的返回值,而且和引用一样,使用的时候必须初始化。当一行多个变量用一个auto声明时,这些变量必须是同一个类型。编译器会把第一个类型进行推导,然后用推导出的类型去定义其他变量。
auto a = 1, b = 2 // 可以 auto x = 1, y = 2.0 // 不可以基于范围的for循环
在C里,当我们要循环一个数组时,往往会用很长的写法。
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
但在C++里,可以写的很简单。
int array[] = { 1, 2, 3, 4, 5 };
for (auto&e : array)
e *= 2;
这里用引用是为了改变原来的值。
空指针nullptrC++给了一个新的空指针nullptr。之前C里的NULL其实是不规范的。因为C的stdio库中,直接将NULL定义为了0或者((void*)0),这会产生一些困难让人觉得莫名其妙的错误。
void f(int)
{
cout << "int" << endl;
}
void f(int*)
{
cout << "int*" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
f(nullptr);
return 0;
}
最后
C++给人的感觉其实是一直在填坑,填C++的坑。



