1. 前言
C++是对C语言的补充和完善;在C语言中,有些问题没有得到很好的解决,在C++中,针对这些问题有了解决方法;因此,在学习C++的语法过程中,会对比着C语言,比较两者的区别
2. 命名空间
在C语言中,我们知道局部变量和全局变量的定义是可以重名的,这时想要打印重名的变量,编译器会以局部优先原则,打印局部变量
C++#include <stdio.h> int x = 0; int main() { int x = 1; printf("%d\n", x);// 输出1 return 0; }
那如果我就是想要打印全局变量的那个值,该怎么办呢?
在C语言中,这个问题没有特别好的解决方案;当然,我们可以将全局变量放在一个函数中,返回该变量的值,但在C++中,引入了命名空间的概念
C++
#include <iostream>
int x = 0;
int main()
{
int x = 1;
printf("%d\n", x);// 1
printf("%d\n", ::x);// 0
return 0;
}
在讲命名空间之间,先来说说C++中域的概念:
C++中有基本的4个域:
- 全局域
- 局部域
- 命名空间域
- 类域
编译器在查找一个变量时:
- 如果该变量没有指定域,那么先去局部域查找,找不到再去全局域查找
- 如果该变量指定了域,那么直接去指定域查找
上面的【::】叫做域作用限定符,如果【::】的前面什么都不加,表示指定了变量x在全局域中,因此编译器就会直接去全局域中找名为x的变量
2.1 命名空间的定义
命名空间的定义与结构体类似,namespae是命名空间的关键字
C++
#include <iostream>
//命名空间的定义
namespace v
{
int x = 0;
}
int main()
{
int x = 1;
printf("%d\n", x);// 1
printf("%d\n", v::x);// 0
return 0;
}
2.2 命名空间用处
上面说到,命名空间可以在两个域的变量重名的情况下,输出指定的域的变量,但命名空间更多是为了解决文件中变量重名的问题
C++
#include <iostream>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
//由于头文件<stdlib.h>展开后,有个rand()函数,跟我们定义的rand变量重名了,因此编译器会报错
//这时可以对变量rand使用命名空间
#include <iostream>
#include <stdlib.h>
namespace v
{
int rand = 10;
}
int main()
{
printf("%p\n", rand);// 打印的是库函数rand()的地址
printf("%d\n", v::rand);// 打印命名空间域中的变量rand
return 0;
}
同样的,在多个文件中,可以会出现重名的变量,结构体,函数名等,这时可以对它们使用命名空间
除了使用命名空间域::变量的方式,我们还可以将命名空间展开
C++
namespace v
{
int a = 10;
int Add(int a, int b)
{
return a + b;
}
struct QNode
{
int val;
struct QNode* next;
};
}
//上面一段代码在其他文件中定义
#include <iostream>
#include "List.h"
using namespace v;// 命名空间展开
int main()
{
printf("%d\n", a);// 10
printf("%d\n", Add(10, 20));// 30
struct v::QNode node1;
return 0;
}
3. C++中的输入输出
C++
#include <iostream>
using namespace std;
int main()
{
int n = 0;
cin >> n;
cout << "n = " << n << endl;
return 0;
}
上面的代码是C++中一个简单的输入输出
- cout,endl,cin是C++库中的函数,使用时必须引用头文件
- cout是标准输出函数(控制台);cin是标准输入函数(键盘);endl表示换行
- << 是流插入运算符;<< 是流提取运算符
相较于C,C++的输入输出函数会自动识别数据类型,不需要像C那样,在输入输出时指定数据类型
C++
#include <iostream>
using namespace std;
int main()
{
char ch = 0;
int i = 0;
double d = 0;
char arr[10] = { 0 };
cin >> ch >> i >> d >> arr;
cout << ch << " " << i << endl;
cout << d << " " << arr << endl;
return 0;
}
//输入c 10 3.2 abcdef
//输出c 10 3.2 abcdef
4. 缺省参数
4.1 缺省参数的概念
C语言中,调用函数时必须写明实参,否则编译器会报错;C++中,可以不写明实参,而在形参中指定一个缺省值;当然,如果指定了实参,形参是实参的值
- 在调用有缺省参数的函数时,可以不指定实参,但如果要指定实参,必须从左往右给,不能间断
C++
int Add(int a = 10, int b = 20)
{
return a + b;
}
int main()
{
int ret = Add();
cout << ret << endl;// 30
ret = Add(1);
cout << ret << endl;// 21
ret = Add(1, 2);
cout << ret << endl;// 3
return 0;
}
4.2 缺省参数的类型
-
全缺省参数:
每个形参都有缺省参数
C++int Add(int a = 10, int b = 20, int c = 30);
-
半缺省参数
部分参数有缺省参数
- 半缺省参数必须从右往左给,不能间断
C++int Add1(int a, int b, int c = 30); int Add2(int a, int b = 20, int c = 30);
注意:同一个形参不能在函数的声明和定义都有缺省参数,否则编译器会报错
C++
//error
int Add(int a, int b = 10);
int Add(int a, int b = 10)
{
return a + b;
}
之后我们写代码,避免不了要在头文件中声明函数,在源文件中定义函数,此时想要使用缺省参数,应该在函数的声明时使用,还是在函数的定义中使用呢?答案是要在函数的声明时使用缺省参数
为什么必须要在函数的声明时使用缺省参数呢?
因为如果是在函数定义使用缺省参数,进行语法检查时,会发现形参和实参不匹配
C++
int Add(int a, int b);
int Add(int a, int b = 10)
{
return a + b;
}
int main()
{
int ret = Add(1);
cout << ret << endl;
return 0;
}
4.3 缺省参数的使用
有了缺省参数,我们更加灵活的使用函数调用
- 我们之前写的顺序表中,起始空间是定好的,当空间不够了进行增容,但如果我已经知道需要多大的空间,比如100个元素的大小,按照原来的代码,还要扩容;在C++中,可以将指定的大小作为缺省参数,一次性开辟
C++
void SeqInit(SeqList* psl, int n = 4);
void SeqInit(SeqList* psl, int n)
{
//...
}
int main()
{
//开辟100个元素的空间
SeqList sl;
SeqInit(&sl, 100);
//开辟10个元素的空间
SeqList s2;
SeqInit(&sl, 10);
//不清楚开辟的空间
SeqList s3;
SeqInit(&sl);
return 0;
}
5.函数重载
C语言中不允许定义同名的函数,在C++中,只要参数不同,即使函数名相同,这两个函数也能正常使用
函数重载的底层原理是什么呢?在讲这个问题之前,需要了解一下文件的编译过程
我们都知道,编译一个文件分为预编译,编译,汇编和链接这几个步骤:
- 预编译:会将头文件展开,宏替换,条件编译,去注释等一系列操作
- 编译:首先从上往下进行语法检查,如果遇到不认识的函数或变量,会报错并停止编译;但如果函数或变量提前声明过,则编译器会通过语法检查,并将源代码转换成汇编代码
- 汇编:将各种符号汇总,生成符号表;如果函数有定义,则会根据函数名和函数地址,生成符号表,而如果函数只有声明,符号表只会包含函数名;最后转换成二进制代码
- 链接:对于没有确定符号表的文件,会根据符号表的函数名去其他文件的符号表一一寻找,如果没有找到,表示链接失败;最后将文件合并
C语言中,仅仅根据函数名来生成符号表;因此如果两个函数的函数名相同,在链接过程中就无法区分两个函数;而C++生成符号表还根据返回值,参数类型,因此即使两个函数的函数名相同,只要参数不同,它们最终生成的符号表就不会相同
不同的编译器符号表的命名规则不同,这里就拿Linux的举例
6. 引用
6.1 引用的概念
C语言有个非常关键的概念,叫指针;想要在函数中修改函数外的内容,C语言中大部分都是使用指针;甚至有些地方不得不使用二级指针;C++创始人Bjarne Stroustrup在使用指针的时候,发现指针不方便使用和代码的阅读,于是在指针的基础上,加入了引用的概念
- 引用,简单点讲是取别名;对一个变量引用,相当于给该变量取别名
C++
int main()
{
int a = 0;
int& b = a;
int& c = b;
return 0;
}
// b和c都是a的别名,三者的地址是一样的,对任意一个变量修改都会影响到其他两个变量
引用的特性:
- 引用的对象必须初始化
- 可以对一个对象多次引用
- 一旦引用一个实体,就不能再引用其他实体
6.2 引用的用法
-
做参数
在C语言中,如果想用函数实现两个数的交换,函数的形参类型必须是指针,同时必须解引用指针;C++中只需将参数改成引用,就可以访问函数外的对象了
C++//C void Swap(int* p1, int* p2) { //... } //C++ void Swap(int& p1, int& p2) { int temp = p1; p1 = p2; p2 = temp; }
-
做返回值
C++int Add(int a, int b) { int c = a + b; return c; } int main() { int ret = Add(1, 2); return 0; }
上述代码中,我们可能会有疑惑,Add函数调用结束后栈帧被销毁了,为什么还能得到c的值;实际上,Add函数在返回之前,将c的值存储在寄存器当中,ret接受到的值其实是寄存器中的值;再来看看下面的代码
C++int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); cout << ret << endl;// 打印3 Add(3, 4); cout << ret << endl;// 打印7 return 0; }
将返回值改成引用,发现并没有修改ret的值自己被修改了
因此,想要用引用做返回值,返回对象的空间必须是栈帧销毁后仍然存在,比如堆,静态区上的空间
6.3 引用和指针的区别
从底层的角度看,引用其实还是指针
- 没有多级引用,有多级指针
- 引用和实体公用一块空间,指针自己有空间
- 引用在定义时必须初始化,指针可以初始化也可以不初始化
- 引用不能改变指向,指针可以
7.内联函数
我们都知道,调用函数需要建立栈帧;但如果函数足够简单,而调用的次数又非常多,建立栈帧效率非常低;C++中被inline修饰的函数叫做内联函数,被修饰后,函数不会开辟栈帧,而是在调用的地方展开,以提高程序的效率
inline是一种以空间换时间的做法,可能会使代码变长,但在一定程度上提高程序效率
inline只适用于规模较小的函数,对于递归或规模大一些的函数,即使加了inline修饰,编译器也不会展开
8.auto关键字
auto可以自动识别数据类型,当一些变量的类型很长,而我们又需要对它进行修改,可以考虑使用auto
C++
int Add(int a, int b){}
int Del(int a, int b) {}
int main()
{
int (*p)(int, int) = Add;
auto p = Del;
return 0;
}
使用auto声明指针时,auto和auto*没有区别;声明引用时,auto必须加&
C++
int main()
{
int a = 10;
auto p1 = &a;
auto* p2 = &a;
auto& c = a;
return 0;
}
9.范围for循环
在C语言中,通常使用下面的方式去遍历数组
C++
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9 };
int sz = sizeof(array) / sizeof(array[0]);
for (int i = 0; i < sz; i++)
{
cout << array[i] << endl;
}
return 0;
}
在C++中,我们可以更加写的更加简化
C++
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9 };
for (auto& e : array)
{
cout << e << endl;
}
return 0;
}
10.C++中的空指针
C++中,指针表示空最好不好使用NULL,因为NULL在C++中被定义成了0,有可能引起传参的错误,而应当使用nullptr,这时C++中定义的关键字,使用时不需要包含头文件
C++
void Fun(int n)
{
cout << "Fun1(int n)" << endl;
}
void Fun(int* p)
{
cout << "Fun2(int* p)" << endl;
}
int main()
{
Fun(0);// 打印Fun1(int n)
Fun(NULL);// 打印Fun1(int n)
Fun(nullptr);// 打印Fun2(int* p)
return 0;
}
0;
}
在C++中,我们可以更加写的更加简化
```C++
int main()
{
int array[] = { 1,2,3,4,5,6,7,8,9 };
for (auto& e : array)
{
cout << e << endl;
}
return 0;
}
10.C++中的空指针
C++中,指针表示空最好不好使用NULL,因为NULL在C++中被定义成了0,有可能引起传参的错误,而应当使用nullptr,这时C++中定义的关键字,使用时不需要包含头文件
C++
void Fun(int n)
{
cout << "Fun1(int n)" << endl;
}
void Fun(int* p)
{
cout << "Fun2(int* p)" << endl;
}
int main()
{
Fun(0);// 打印Fun1(int n)
Fun(NULL);// 打印Fun1(int n)
Fun(nullptr);// 打印Fun2(int* p)
return 0;
}