命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
命名空间的概念
命名空间是一个声明区域,它可以将变量、函数、类等标识符组织在一起,形成一个独立的作用域。这意味着,在同一个程序中,不同命名空间内的标识符可以同名而不会产生冲突。
命名空间的作用
- 避免命名冲突:在一个大型的项目中,不同的模块或库可能会使用相同的函数名或变量名,命名空间能够有效地避免这种冲突。
- 提高代码的可读性和可维护性:通过将相关的标识符组织在同一个命名空间中,代码的结构更清晰,便于管理和维护。
命名空间的使用
定义命名空间
在C++中,使用namespace
关键字来定义一个命名空间:
namespace 名称 {
// 声明或定义变量、函数、类等
}
例如:
namespace MyNamespace {
int add(int a, int b) {
return a + b;
}
}
访问命名空间中的成员
要访问命名空间中的成员,可以使用作用域解析操作符::
MyNamespace::add(5, 10);
使用using
声明
可以使用using
关键字简化对命名空间成员的访问:
using namespace MyNamespace;
add(5, 10); // 直接使用函数名,无需加命名空间前缀
使用匿名命名空间
匿名命名空间是一种特殊的命名空间,它没有名字,通常用于隐藏只在当前源文件中使用的函数、变量和类型。在匿名命名空间中声明的所有元素都自动成为该源文件的局部命名空间的一部分:
namespace {
int hiddenVariable = 42;
void hiddenFunction() {
// 函数体
}
}
在这个例子中,hiddenVariable
和hiddenFunction
只能在当前源文件内部访问。
C++的输入和输出
C++标准库中的所有标准函数和对象都放在名为std
的命名空间中,这是为了防止它们与程序中定义的标识符产生冲突。例如,使用std::cout
和std::endl
来输出到控制台。
c++的Hello world
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{
cout<<"Hello world!!!"<<endl;
return 0;
}
说明:
-
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件
以及按命名空间使用方法使用std。
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件
-
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<
iostream >头文件中。
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<
-
- <<是流插入运算符,>>是流提取运算符。
-
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型。
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
// 可以自动识别变量的类型
cin>>a;
cin>>b>>c;
cout<<a<<endl;
cout<<b<<" "<<c<<endl;
return 0;
}
std命名空间的使用惯例
std是C++标准库的命名空间,如何展开std使用更合理呢?
- 在日常练习中,建议直接using namespace std即可,这样就很方便。
- using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。
缺省参数
缺省参数是函数定义时的一个特性,它允许在函数的声明或定义中为某些参数指定默认值。如果没有为这些参数传递实参,函数就会使用这些默认值;如果传递了实参,则使用传递的值。
缺省参数的定义
当你定义或声明一个函数时,你可以为参数指定一个默认值,如下所示:
void Print(int number = 0) {
cout << number;
}
在这个例子中,函数Print
有一个参数number
,它的默认值是0。这意味着如果你调用Print()
而没有任何实参,函数将使用默认值0。
缺省参数的使用
调用函数时不传递参数
如果你在调用Print
函数时不提供任何参数,它将使用参数的默认值:
Print(); // 输出: 0
调用函数时传递参数
如果你提供了参数,函数将使用你传递的值而不是默认值:
Print(5); // 输出: 5
缺省参数的规则
-
默认值的位置:在函数原型或定义中,默认值必须位于参数列表的末尾。也就是说,你必须从右向左指定参数的默认值。
-
全缺省参数:所有参数都可以有默认值,这种函数调用时可以不提供任何实参。
-
半缺省参数:只有部分参数有默认值。当你指定了某个参数的默认值时,所有位于其右侧的参数也必须有默认值。
-
默认参数的顺序:你不能为一个参数指定默认值,然后跳过前面的参数为后面的参数指定默认值。必须从右至左连续指定。
示例
下面是一个包含半缺省参数的函数示例:
void Print(int number, int count = 1) {
for (int i = 0; i < count; i++) {
cout << number << " ";
}
cout << endl;
}
Print(5); // 输出: 5 (使用了number的默认值,count的默认值)
Print(5, 3); // 输出: 5 5 5 (使用了传递的number值和count值)
在这个例子中,Print
函数的第一个参数number
没有默认值,第二个参数count
有默认值1。所以,如果你只提供一个参数,那么第二个参数将使用默认值1。
函数重载
函数重载是一种允许多个同名函数在相同的作用域中存在,只要它们的参数列表不同(即参数的个数、类型或顺序不同)的特性。函数重载使得程序设计更加灵活,能够根据不同的参数执行相似或相关的任务。
函数重载的概念
当你在同一作用域内定义多个同名函数时,这些函数如果参数列表不同,它们就是重载的。重载的函数可以具有不同的参数数量、不同的参数类型,或者参数类型顺序不同。然而,函数重载与函数返回类型无关,即不能仅通过返回类型来重载函数。
函数重载的规则
-
参数列表必须不同:重载的函数必须有区别于其他同名函数的参数列表。
-
返回类型不影响重载:即使函数的返回类型不同,也不构成有效的重载。
-
作用域的限制:函数重载只限于同一作用域内。在不同的作用域(如不同的命名空间或类中),即使是完全相同的函数名和参数列表,也被视为不同的函数。
函数重载的示例
以下是一个展示函数重载的简单示例:
#include <iostream>
using namespace std;
// 重载函数:接受一个整数参数
void display(int number) {
cout << "Displaying int: " << number << endl;
}
// 重载函数:接受浮点参数
void display(float number) {
cout << "Displaying float: " << number << endl;
}
// 重载函数:接受两个整数参数
void display(int number1, int number2) {
cout << "Displaying two int: " << number1 << " and " << number2 << endl;
}
int main() {
display(12); // 调用第一个重载版本的display函数
display(12.3f); // 调用第二个重载版本的display函数
display(12, 24); // 调用第三个重载版本的display函数
return 0;
}
在这个例子中,我们定义了三个display
函数。第一个接受一个整数参数,第二个接受一个浮点参数,第三个接受两个整数参数。由于它们的参数列表不同,这三个函数是重载的。
函数重载的工作原理
编译器通过函数的名称修饰(name mangling)来区分不同的载函数。它修改函数的名称,通常添加额外的信息以反映参数类型和参数数量,从而生成不同的函数名称以供链接器使用。
函数重载的注意点
- 不能通过返回类型来区分重载函数。
- 如果新的重载函数参数列表是现有函数参数列表的超集,这可能导致函数名称修饰出现问题,并可能引起函数调用的歧义。
-重载函数时,要注意不要造成函数名称的混淆,否则编译器可能无法正确地选择调用哪一个函数。
引用
当然可以。在C++中,引用(reference)是一个非常便利的特性,它允许程序员为变量的别名。通过使用引用,我们可以通过这个别名来直接访问或修改变量的值。引用在很多情况下可以用来代替指针,因为它们提供了一种更安全和更易于理解的语法。
引用的定义和声明
引用的声明与指针类似,但它不需要使用星号*
。引用在声明时必须被初始化,一旦初始化之后,它就不能再绑定到另一个变量上。
类型 &引用名 = 变量名;
例如:
int a = 10;
int &ref = a; // 声明引用ref,并将其初始化为变量a的引用
在这个例子中,ref
是a
的引用,它们代表同一个存储位置。
引用的特性
-
引用是别名:引用实际上是原变量的别名,任何对引用的操作都直接反映在原变量上。
-
初始化时绑定:引用必须在声明时绑定到具体的变量,且一旦绑定就不能再改变引用所指向的变量。
-
没有独立的内存地址:引用没有自己的内存地址,它共享原变量的内存地址。
-
无需解引用:使用引用时不需要像指针那样进行解引用操作,直接使用引用名即可。
引用的使用
传递引用到函数
引用经常用于函数参数,允许函数直接修改传入的参数值,而不是参数的副本:
void addOne(int &num) {
num = num + 1; // 直接修改传入的参数
}
int main() {
int x = 5;
addOne(x); // x的值将变为6
return 0;
}
在这个例子中,addOne
函数接受一个整数的引用作为参数,所以它可以直接修改x
的值。
返回引用
函数也可以返回引用,这样返回的是变量的引用而不是副本:
int &getFirstElement(int arr[]) {
return arr[0]; // 返回第一个元素的引用
}
int main() {
int myArray[] = {10, 20, 30};
int &first = getFirstElement(myArray); // first是myArray[0]的引用
first = 100; // 直接修改myArray[0]的值
return 0;
}
在这个例子中,getFirstElement
函数返回数组第一个元素的引用,因此通过first
可以直接修改myArray[0]
的值。
引用的注意事项
- 不能有null引用:指针可以被赋予
nullptr
,但引用必须指向一个有效的变量。 - 不能重新绑定:一旦引用被初始化为一个变量,它就不能再指向另一个变量。
- 引用不能指向数组:你不能创建一个数组的引用,但可以创建指向数组元素的引用。
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体 - 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节) - 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
内联函数
内联函数是一种优化手段,其目的是减少函数调用的开销。当函数体较小且被频繁调用时,函数调用的成本可能会对性能产生负面影响。为了减少这种开销,可以将函数声明为内联函数,使得每次函数调用时,编译器将函数的代码直接插入到调用点,而不是通过跳转到函数代码的地址去执行。
内联函数的定义
内联函数通过在函数声明或定义之前添加inline
关键字来指定。例如:
inline int add(int a, int b) {
return a + b;
}
内联函数的特点
-
减少调用开销:由于函数体直接插入到调用点,省去了函数调用的参数传递、返回值处理以及堆栈操作等开销。
-
无额外内存分配:内联函数不会像普通函数那样在栈上为局部变量分配内存。
-
编译器优化:编译器可能会对内联函数的代码进行额外的优化。
内联函数的使用
内联函数通常用于以下场景:
-
小型函数:函数体非常短小,例如简单的算术运算或访问器函数。
-
频繁调用的函数:如果一个函数在程序中频繁调用,那么将其声明为内联可能会提升性能。
-
性能关键代码:在性能敏感的代码路径中,使用内联函数可以减少函数调用开销。
内联函数的注意事项
-
编译器选择:即使函数被声明为内联,编译器也有权决定是否真正内联该函数。这取决于编译器的优化策略和函数的复杂性。
-
过度内联:如果过度使用内联函数,可能会导致代码膨胀,增加程序的内存占用,并且可能影响缓存效率。
-
递归函数:内联递归函数可能会导致代码无限增长,因此通常不推荐对递归函数使用内联。
-
调试困难:内联函数可能会使得调试变得复杂,因为函数调用点没有明显的调用栈帧。
内联函数与宏的区别
虽然内联函数和宏都可以在编译时插入代码,但它们有一些重要区别:
-
类型安全:内联函数是类型安全的,而宏不是。宏只是简单的文本替换,可能会导致类型安全问题。
-
作用域:内联函数可以访问局部变量和全局变量,而宏则不能。
-
调试:内联函数可以像普通函数一样调试,宏则不能。
-
参数的副作用:宏在展开时可能会对参数产生副作用,而内联函数则不会。