目录
1. C++关键字
2. 命名空间
2.1 命名空间的定义
2.2 命名空间的访问
2.3 命名空间的使用
3. C++的输入和输出
4. 缺省参数
4.1 缺省参数的概念
4.2 缺省参数的分类
5. 函数重载
5.1 函数重载的概念
5.2 函数重载的原理
5.3 C语言内部函数名修饰规则
5.4 C++内部函数名修饰规则
6. 引用
6.1 引用的概念
6.2 引用的特征
6.3 引用的使用场景
6.4 传值、传引用效率
6.5 引用和指针的区别
1. C++关键字
C++合计共有63个关键字,而C语言一共有32个关键字,如下:
2. 命名空间
- 在C语言中,当函数名,变量名存在冲突的时候,C语言没有很好的办法来解决这一问题,例如以下一段代码:
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> int rand = 10; int main() { printf("%d\n", rand); return 0; }
- 在stdlib.h 这个头文件中,存在rand()函数,与全局变量的 rand 存在命名冲突,因此在编译过程中会报错:
- 因为rand()函数本质上是一个地址,编译器希望以%p的形式打印,而不是%d
- 然而当以%p的形式输出的时候又会报重定义的错误:
- 为了解决这个问题,C++中采用了 命名空间这个方法。
2.1 命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}可,{}中即为命名空间的成员,例如:
namespace D1 { int rand = 10; }
命名空间中可以定义变量,函数,类型,例如:
namespace D1 { // 命名空间中可以定义变量/函数/类型 int rand = 10; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; }
2.2 命名空间的访问
- 那么命名空间应该如何访问呢?这时需要引用一个操作符 :: 域作用限定符。
- 在C语言中当全局变量和局部变量存在冲突的时候,编译器的寻找顺序是先在局部寻找,如果找不到就会在全局中寻找,例如:
- 那如何访问全局中的变量呢?这时候就要用到 域作用限定符:
当与作用限定符前没有命名空间的名字的时候,默认访问全局。
类似的,如果 域作用限定符前有命名空间,那么就会访问此命名空间中的内容,例如:
2.3 命名空间的使用
- 实际上,命名空间一共有三种使用方法,
- 第一种方法刚才已经提到,就是 命名空间名 :: 参数 ;
- 第二种就是 使用using namespace 命名空间名称引入,例如:
using namespace D1; int main() { int x = 6; //struct D1::Node phead; std::cout << D1::Add(10, 20) << std::endl; //printf("%d\n", D1::x); return 0; }
- 第三种就是使用using将命名空间中某个成员引入,例如:
//test.h namespace D1 { int a = 10; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; } // test.c using D1::a; int main() { std::cout << a << std::endl; return 0; }
3. C++的输入和输出
#include<iostream> // std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中 using namespace std; int main() { cout << "Hello world!!!" << endl; return 0; }
#include<iostream> // std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中 using namespace std; int main() { const char* str = "Hello World!!!"; const char* str1 = "你好世界!"; int i = 10 >> 1; int h = 0; cin >> i >> h; cout << i << " " << h << endl; cout << str << " " << str1 << endl; return 0; }
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
- 2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<iostream>头文件中。
- 3. << 是流插入运算符,>> 是流提取运算符。
4. 缺省参数
4.1 缺省参数的概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
int Add(int x = 0, int y = 100) { return x + y; } int main() { int a = 10; int b = 10; int ret1 = Add(a, b); int ret2 = Add(a); cout << "ret1 = " << ret1 << endl << "ret2 = " << ret2 << endl; return 0; }
4.2 缺省参数的分类
- 全缺省参数
int Add(int x = 0, int y = 100) { return x + y; } int main() { int a = 10; int b = 10; int ret1 = Add(a, b); int ret2 = Add(a); int ret3 = Add(); cout << "ret1 = " << ret1 << endl << "ret2 = " << ret2 << endl; return 0; }
- 半缺省参数
void Func(int a, int b = 10, int c = 20) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { Func(10); cout << endl; Func(10, 20); cout << endl; Func(20, 40, 60); return 0; }
当我们在写一些程序或者项目的时候喜欢把函数声明和定义分开写,那么能否在函数声明和定义的时候同时缺省参数呢?答案是不行的。
编译器在编译函数的时候,会经历四个过程,分别是 预处理,编译,汇编,链接
- 预处理:预处理过程会将 .c /.cpp 文件生成 .i 文件,此时会处理程序中的 宏,注释 以及头文件的展开 等预处理信息。
- 编译: 编译过程会将 .i 文件生成 .s 文件,并且会将所有的语言翻译成汇编语言,翻译 的同时便会检查语法是否正确,正常会有以下阶段:词法分析、语法分析、语义 分析、符号汇总。这里的 符号汇总 就会整理文件中所有出现的函数。(此时只 会检查函数的声明是否正确。
- 汇编:汇编过程会将 .s 文件 生成 .o 文件,同时会将汇编语言转化成机器能识别的二进 制代码,生成目标文件,并将符号整理到表格中,生成符号表 。
- 链接:链接的过程会生成.out文件,在gcc的环境下,目标文件通常是按照 elf 的格式存 放的,此时会把目标文件合并,即合并段表,其次还会把所有的 符号汇总。
缺省参数不能在函数声明和定义的时候同时给,一般会在函数声明的时候给,因为在编译的过程中,编译器会检查语法,如果存在这个函数的声明,则编译的时候会通过,但是不会找到函数的地址,函数的地址是什么呢?函数在编译的过程中会被会变成一条条指令,那函数的地址就是这些指令的第一条指令的地址。只有在链接的过程中,会将所有的目标文件和所有的符号汇总,通过符号表找到对应的函数的地址。
比如以下代码:
void Func(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
Func(10);
cout << endl;
Func(10, 20);
cout << endl;
Func(20, 40, 60);
return 0;
}
一定要注意:
- 半缺省参数必须从右往左依次来给出,不能间隔着给;
- 缺省参数不能在函数声明和定义中同时出现。
5. 函数重载
5.1 函数重载的概念
我们知道在C语言中不允许同名参数的出现,在C++中,出现了重载的概念:
- 函数重载:函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类 似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不 同,常用来处理实现功能类似数据类型不同的问题。
- 函数参数类型不同
// 1、参数类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
- 函数参数个数不同
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
- 函数参数类型顺序不同
// 3、参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
5.2 函数重载的原理
我刚才提到过,每个源文件都会通过编译器生成目标文件,例如现在有两个.cpp文件,一个是a.cpp另一个是b.cpp,当我们要从a.cpp文件中调用b.cpp文件中的 某个函数时,在编译之后链接之前a.o中都不会有这个函数的地址,这个函数的地址存放在b.o中,那么怎么办呢?
这个时候会通过链接的过程,才能找到这个函数的地址,链接器看到a.o要调用这个函数,但是在本文件中并没有这个函数的定义,就会到b.o中找到这个函数的地址,然后连接到一起。
那么链接的时候,编译器会通过什么名字去寻找呢?如果函数的名字都一样,那么在底层是如何找到我们需要调用的函数的呢?
5.3 C语言内部函数名修饰规则
我们以Linux下的gcc为例:
- 这是我在gcc的环境下编写的一段C代码用一下命令进行编译:
gcc -o testc test.c
- 生成以下文件:
- 再输入以下命令查看汇编代码:
objdump -S testc
- 就可以查看函数的命名方式:
5.4 C++内部函数名修饰规则
- 类似的,我又写了一段C++代码:
- 再使用以下命令进行编译:
g++ -o testcpp test.cpp
- 生成以下文件:
- 再使用以下命令查看汇编代码:
objdump -S testcpp
- 查看C++函数命名方式:
所以很容易看出来:
- 在Linux环境下,gcc编名和程序员本身取得名字一样;
- 在Linux环境下,g++编名函数名修饰方式发生变化,会以_Z开头,函数名的字节数放在后面,之后就是函数的返回类型的字母的缩写。
当然,在Windows环境下,C++函数名字修饰规则更加复杂,但道理都是类似的
6. 引用
6.1 引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
- 类型& 引用变量名(对象名) = 引用实体,例如:
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
6.2 引用的特征
- 引用在使用的时候必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,之后就不可以引用其他实体
6.3 引用的使用场景
-
引用做参数
示例1:
void Swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } int main() { int a = 10, b = 20; cout << "a = " << a << " " << "b = " << b << endl; Swap(a, b); cout << "a = " << a << " " << "b = " << b << endl; }
示例2:
例如我们在写链表的时候一般会使用二级指针传参来改变plist,但是有了C++的引用,就可以把引用传过去:
typedef struct Node { struct Node* next; struct Node* prev; int val; }LNode, *PNode; void PushBack(PNode& phead, int x) { } int main() { PNode plist = NULL; return 0; }
- 引用做返回值
示例1:
如果函数有返回值,那在返回的过程中不会直接返回那个变量,而是把那个变量临时拷贝到另一个变量中,并且这个变量通常是不可变类型的,例如:
int Add(int x = 0, int y = 0) { int ret = x + y; //因为函数出了栈帧就会销毁,所以这个值通常会被存在另一个临时变量中 return ret; }
但如果以引用的形式返回,就会大大提高效率,因为引用只是别名,语法上不会开辟另一个空间来存放返回值 。
int& Add(int x = 0, int y = 0) { static int ret = x + y; return ret; }
注意这里定义的是一个static类型的变量,如果定义的类型只是普通的int类型,因为出了函数栈帧就会被销毁那么这块int 的空间就会被重新分配给操作系统,就变成了野引用,而static类型不是存放在栈中,因此出了函数也不会被销毁。
6.4 传值、传引用效率
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include <time.h> struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A)-time:" << end2 - begin2 << endl; } int main() { TestRefAndValue(); return 0; }
6.5 引用和指针的区别
- 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
int main()
{
int a = 10;
int* pa = &a;
int& ra = a;
ra = 20;
*pa = 30;
return 0;
}
- 在底层实现上实际是有空间的,因为引用是按照指针方式来实现的
C++的引用,对指针使用比较复杂的场景进行一些替换,让代码简单易懂,但不能完全替代指针,原因是引用定义后,不能改变指向。
区别如下: