【C++】初识C++
文章概括
谈谈C++在学习前的认知,C++是在C的基础上,容纳进去了面向对象的编程思想,并增加了许多有用的库,以及编程范式等等。所以学习C++之前一定对C有一定的认知,一个好的C++程序员一定会是一个优秀的C语言程序员。
本章主要介绍:补充C语言语法的不足,以及C++是如何对C语言程序设计不合理地方进行优化的,比如:作用域方面、IO方面、函数方面、指针方面、宏方面等等;同时也为后续学习类和对象做了铺垫。
关键字(C++98)
C++有63个关键字,C语言有32个关键字。
关键字 | 关键字 | 关键字 | 关键字 | 关键字 |
---|---|---|---|---|
asm | auto | bool | break | case |
catch | char | class | const | const_cast |
delete | do | double | dynamic_cast | else |
enum | explicit | export | extern | false |
float | goto | if | inline | int |
long | mutable | namespace | new | operator |
private | protected | reinterpret_cast | return | short |
signed | sizeof | static | static_case | struct |
switch | template | this | try | typedef |
typeid | typename | union | unsigned | using |
virtual | void | volatile | continue | for |
public | throw | wchar_t | default | friend |
register | true | while |
后续逐渐了解
命名空间
命名空间的定义
先以C语言举例:
假设需要定义一个全局变量随机数random为10
#include<stdio.h> int rand = 10; int main(void) { printf("%d\n", rand); return 0; }
这是可以编译成功的,但是我们之前有了解过rand是一个头文件stdlib.h的一个库函数,如果我们包含stdlib.h这个头文件会发生什么?
#include<stdio.h> #include<stdlib.h> int rand = 10; int main(void) { printf("%d\n", rand); return 0; }
发生报错,这里可以明显突出一个C语言的库命名冲突问题。
有时在一个大的工程中有多个项目,每个项目会由不同的人负责,这时也会难免遇到项目之间的命名问题。
总之,C语言命名冲突的问题有:
1.库命名冲突问题
2.项目相互之间命名的冲突
在C++中,存在命名空间namespace可以解决这类型的问题。
在讲解命名空间前,需要先了解域的概念:域可以看作是一个作用区域,域包含类域、命名空间域、局部域、全局域等等
在一般情况下访问时,会先访问局部域,在局部域中未发现变量,会进而访问全局域。
假设在全局域中存在全局变量,同时在局部域中也存在一个局部变量,但是想要跳过局部域直接访问全局域,应该如何操作?
int a = 1; int main(void) { int a = 0; printf("%d\n", ::a); return 0; }
这里需要介绍一个操作符"::",域操作限定符,::a默认会跳过局部域,访问全局域。
那如果存在命名空间域namespace,其优先级是如何?
int a = 1; namespace project { int a = 2; } int main(void) { int a = 0; printf("%d\n", a); printf("%d\n", ::a); return 0; }
由此可见,访问变量a,会先访问局部域——>然后访问全局域——>最后在默认情况下,编译器并不会主动去命名空间域搜索。
想要搜索命名空间域,有俩种方式:
1.展开命名空间域
namespace project { int a = 2; } using namespace project; int main(void) { printf("%d\n", a); return 0; }
2.指定访问命名空间域
namespace project { int a = 2; } int main(void) { printf("%d\n", project::a); return 0; }
如果在局部域,全局域,命名空间域(展开命名空间域)中都存在变量a,会如何访问?
这里可以发现局部域的优先级最高,但如果只存在全局域与展开后命名空间域时,会发生报错,原因在于:展开的命名空间域相当于暴露在全局域。
所以不要轻易使用using namespace + 名,即不要请轻易展开命名空间域。
总结:在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
命名空间的特性
- 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
namespace project { int a = 2; }
1.命名空间域中可以定义变量、函数、类型
namespace project { //定义变量 int num = 10; //定义函数 int add(int x, int y) { return x + y; } //定义结构体 struct Node { struct Node* next; int data; }; }
2.命名空间可以嵌套
namespace project { namespace N1 { int a = 1; } namespace N2 { int a = 2; //定义函数 int add(int x, int y) { return x + y; } } } int main(void) { printf("%d ", project::N1::a); printf("%d ", project::N2::a); printf("%d ", project::N2::add(1,2)); return 0; }
- std是C++标准库的命名空间名,C++将标准库的定义实现都放在这个命名空间里。
【注意】:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
3.同一个工程中允许存在多个相同的命名空间,编译器最后会合成同一个命名空间,即可以在多个文件中定义相同名字的命名空间
输入与输出
C++中的输入输出
早年在VC6.0时没有命名空间,头文件C++中的头文件
#include<iostream.h>
后面改为了
#include<iostream> #include<vector> #include<list>
使用iostream这个头文件时,需要先学习C++的输入输出.
c语言中使用printf与scanf来将数据输出与输入,而在C++中使用cout与cin实现输入输出。
#include<iostream> using namespace std; int main(void) { int a; cin >> a; cout << a << endl; return 0; }
说明:
1.使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含头文件iostream,以及按照命名空间使用方法使用std。
2.cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,它们都包含在iostream头文件中
3.<<是流插入运算符,>>是流提取运算符
4.cout和cin的使用比较方便,不需要同printf与scanf一样手动控制格式,C++的输入输出可以手动控制变量类型
输入输出的命名空间
使用输入输出有3种情况:
1.指定访问命名空间域
#include<iostream> int main(void) { std::cout << "Hello World!" << std::endl; return 0; }
2.使用展开命名空间域
#include<iostream> using namespace std; int main(void) { cout << "Hello World!" << endl; return 0; }
编译器会去std这个命名空间搜索(std这个命名空间域中封有iostream)
【注意】直接展开std会有很大的风险,当存在自己定义的名字与库中名字重合会报错,建议项目中不要展开,日常使用可以进行展开,项目中建议指定访问,不要轻易展开命名空间。
3.展开部分命名
#include<iostream> using std::cout; using std::endl; int main(void) { cout << "Hello World!" << endl; return 0; }
缺省参数
缺省参数也称默认参数,即函数在传参的时候可以存在缺省参数(默认参数)。
void Init(int* node, int sz = 4) { int* newnode = (int*)malloc(sizeof(int) * sz); if (newnode == NULL) { perror("malloc fail"); return; } node = newnode; } int main(void) { int* node; //默认情况下,初始化4个字节 Init(node); //可以指定实参,初始化100个字节 Init(node, 100); return 0; }
观察代码,在C++中传参存在俩种情况:
1.没有参数时,使用参数的默认值
2.有任何参数时,使用指定的实参
即实参的优先级最大,当不存在实参时,使用默认参数
【注意】当存在多个缺省参数时,不允许跳着传参,只能从左到右顺序传参
#include<iostream> using namespace std; //全缺省 int RetAdd(int a = 1, int b = 2, int c = 3) { return a + b + c; } int main(void) { int sum = RetAdd(); cout << sum << endl; return 0; }
#include<iostream> using namespace std; //半缺省 int RetAdd(int a, int b, int c = 3) { return a + b + c; } int main(void) { int sum = RetAdd(1,2); cout << sum << endl; return 0; }
C++中全缺省与半缺省的概念
全缺省:所有的参数都给了缺省值
半缺省:缺省部分参数
【注意】半缺省参数必须从右至左依次缺省,切勿间隔缺省
【注意】在使用缺省参数使用需要注意,声明与定义不能同时给缺省值,一般在声明时存在缺省值,定义时不存在缺省值。
如果了解文件的编译与链接可以知道,编译期间只能看到声明,链接期间可以看到定义
函数重载
重载的意思是:一词多义
那么函数重载:是函数的一种特殊情况,C++允许在同以作用域中声明几个功能类似的同名函数,这些函数的形参列表(参数个数、参数类型、类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
- 参数类型不同
#include<iostream> using namespace std; //参数类型不同 int RetAdd(int a, int b) { return a + b; } double RetAdd(double a, double b) { return a + b; } int main(void) { cout << RetAdd(1, 2) << endl; cout << RetAdd(1.5, 2.2) << endl; return 0; }
- 参数个数不同
#include<iostream> using namespace std; //参数个数不同 void Fun() { cout << "无参数" << endl; } void Fun(int a) { cout << "有参数" << endl; } int main(void) { Fun(); Fun(1); return 0; }
- 类型顺序不同
#include<iostream> using namespace std; //类型顺序不同 double RetAdd(int a, double b) { return a + b; } double RetAdd(double a, int b) { return a + b; } int main(void) { cout << RetAdd(1, 2.3) << endl; cout << RetAdd(1.9, 2) << endl; return 0; }
【注意】有三个不构成函数重载的例子
1.仅返回值不同
2.仅变量名不同
3.不明确的函数调用
引用
引用的概念
引用不是新定义一个变量,而是给已存在变量取一个别名,编译器不会为引起变量开辟内存空间,它和它的变量共用同一块内存空间。
#include<iostream> using namespace std; int main(void) { int a = 10; int& b = a; int& c = b; int& d = a; printf("%p\n", &a); printf("%p\n", &b); printf("%p\n", &c); printf("%p\n", &d); return 0; }
类型& : 引用变量名(对象名) = 引用实体
在学习C语言阶段,想要找到一个变量,可以使用指针,而在C++中引入了引用的概念,可以大幅度代替指针的作用。
int main(void) { int a = 0; int* pa = &a; int** ppa = &pa; printf("%p\n", &a); printf("%p\n", pa); printf("%p\n", *ppa); return 0; }
引用的特性
1.引用在定义时必须需要初识化在这里插入图片描述
2.一个变量可以有多个引体
#include<iostream> using namespace std; int main(void) { int a = 10; int& b = a; int& c = a; int& d = a; return 0; }
3.引用一旦引用一个实体,便不可再引用其他引体
#include<iostream> using namespace std; int main(void) { int a = 10; int x = 20; int& b = a; b = x; cout << b << endl; return 0; }
引用地使用场景
引用做参数
引用做参数时,可以作为输出型参数使用,何为输出型参数?
函数在传参地时候,会传输出型参数或者输入型参数,输入型参数的意思是形参的改变不可以改变实参,即形参是实参的一份临时拷贝;输出型参数的意思是形参的改变要改变实参。
在C语言中一般使用指针来做输出型参数:
//链表 typedef struct ListNode { int data; struct ListNode* next; }PNode; void LTPushBack(PNode* p, int x);
而在C++中,可以使用引用来做输出型参数:
//链表 typedef struct ListNode { int data; ListNode* next; //在C++中可以不写struct }*PNode; void LTPushBack(PNode& p, int x);
这段代码的意思是:定义一个结构体的指针,引用这个指针并使用phead作为别名。
引用做参数,同时也可以提高效率,但是只存在于数量较大对象或者深拷贝类对象。
下面这段代码可以比较传值与传引用的效率对比:
#include <time.h> struct A { int a[100000]; }; 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; }
由于函数在传参期间,以值作为参数,函数不会之间传递实参或者将变量本身直接返回,而是传递实参的一份临时拷贝,因此以值作为参数时的效率是很低的,尤其是参数非常大时,效率会更低。
对象越大,代价就会越大,提高的效率就越多
【注意】引用相比较于指针没有质的区别,但是在C++中实际情况下引用的使用情况较多。
引用做返回值
int Count() { static int n = 0; n++; return n; } int main(void) { int ret = Count(); return 0; }
观察这段代码,使用传值返回时,会生成临时变量,可能会存放在寄存器中(寄存器的大小为4/8个字节,如果寄存器内存不够,会向上申请),作为int ret = Count();这段代码的返回值。
虽然n被static int修饰,成为静态变量,存放在静态区中,但是不管有没有static修饰或者存在全局变量,函数都会根据返回值int,创造出一个临时变量,这样会大大降低效率,那么使用引用做返回值可以解决这样的问题。
下面就是引用做返回值的第一个作用:
- 减少拷贝,提高效率
int& Count() { static int n = 0; n++; return n; } int main(void) { int ret = Count(); return 0; }
下面这段代码可以比较传值返回与传引用返回:
#include <time.h> struct A{ int a[10000]; }; A a; // 值返回 A TestFunc1() { return a;} // 引用返回 A& TestFunc2(){ return a;} void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; }
由于函数在传值返回期间,以值作为返回值,函数不会之间传递实参或者将变量本身直接返回,而是返回变量的一份临时拷贝,因此以值作为返回值时的效率是很低的,尤其是参数非常大时,效率会更低。
使用引用做返回值,还有另外一个作用:
- 修改返回值以及获取返回值
typedef struct SeqList { int arr[100]; int size; }SL; int& SLPostion(SL& s, int pos) { assert(pos < 100 && pos >= 0); return s.arr[pos]; } int main(void) { SL s; SLPostion(s, 0) = 1; int ret = SLPostion(s, 0); cout << ret << endl; cout << SLPostion(s, 0) << endl; return 0; }
如果不使用static修饰静态变量,使用引用做返回值时,结果时不确定的。
int& Count() { int n = 0; n++; return n; } int main(void) { int& ret = Count(); cout << ret << endl; return 0; }
这段代码虽然不会报错,但是明显可以发现,ret访问是存在越界访问。
int& Count(int n) { n++; return n; } int main(void) { int& ret = Count(10); cout << ret << endl; Count(20); cout << ret << endl; return 0; }
观察这段代码,如果count函数结束,函数建立的栈帧会销毁,在vs编译器上没有清理栈帧,ret的值是第一次函数调用结束后,第二次函数建立在同样的位置。
int& Count(int n) { n++; return n; } int main(void) { int& ret = Count(10); cout << ret << endl; rand(); cout << ret << endl; return 0; }
若是随意插入一个函数,则ret的值变成了随机值。
可以发现,在这种情况下使用引用是很危险的。
总结:
1.基本任何场景都是可以使用引用传参
2.谨慎用引用做返回值,出了函数作用域,对象不在了就不能使用引用返回,如果对象还在就可以使用引用返回。
2.可以使用引用返回的场景:静态变量、全局变量、堆区malloc或者calloc
常引用
const int a = 0; int& b = a;
观察这段代码,当变量a被const修饰变成不可修改的左值时,使用int引用是不可以的,原因是在引用的过程中国权限不能放大。
int a = 0; int& b = a;
const int a = 0; const int& b = a;
这俩种情况是被允许的,这俩种情况是权限的平移。
int a = 0; const int& b = a;
这种情况也是被允许的,这种情况被称为权限的缩小。
int a = 0; const int& b = a; a++;
这种情况也是被允许的,原因是编译器仅缩小了a和b所在地址的引用b的权限,而并没有缩小a的权限所有a++是被允许的,而b++是不被允许的。
const int& x = 10;
同时,给变量取别名也是被允许的。
常引用的俩个例子
double d = 1.11; int i = d;
我们都知道double在转变为int时,会进行类型转换,由于double是8个字节,int是4个字节,double变成int会进行截断,而截断的过程会建立一个新的临时变量,临时变量具有常性,即临时变量是不可修改的值。
double d = 1.11; int& i = d;
所有这段代码是错误的,这里double变成临时变量权限缩小,而int&会将权限放大,引用的过程中权限是不可以放大的。
double d = 1.11; const int& i = d;
这段代码是正确的,这里进行了权限的平移。
int Fun() { static int x = 0; return x; } int main(void) { int& ret1 = Fun(); const int& ret2 = Fun(); }
第二个例子是关于函数在释放前会建立一个临时变量给返回值提供位置。此时这个临时变量也是具有常性,是不可以修改的,即ret1是错误的代码,而ret2是正确的代码。
引用与指针的区别
引用与指针在语法层面是不同的,引用不开空间,引用是对变量取别名,而指针不同,指针开空间,指针是存储变量的地址。
int x = 10; int* y = &x; int a = 20; int& b = a;
观察这段代码,可以发现从底层汇编指令实现的角度来看,引用是类似指针的方式实现的。
- 指针与引用的不同点:
1.引用概念上定义一个变量的别名,而指针存储一个变量的地址
2.引用在定义时必须初始化,而指针没有要求
3.引用在初始化时引用一个实体后,就不可以引用其他实体,而指针可以在任何时候指向任何一个同类型的实体
4.没有NULL引用,但有NULL指针
5.引用和指针在sizeof中含义不同,引用结果为引用类型的大小,而指针始终是地址空间所占字节个数(32位平台下占4个字节)
6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个指针类型的位置
7.有多级指针,但没有多级引用
8.访问实体的方式不同,指针需要解引用,引用编译器会自己处理
9.引用比指针使用起来相对安全
内联函数
在了解内联函数之前,应该对C语言中的宏的定义有一定了解。
//宏定义 #define ADD(x,y) ((x) + (y)) * 10 //注意规范,宏定义是完整的替换 int main(void) { for (int i = 0; i < 100; i++) { cout << ADD(i, i + 1) << endl; } return 0; }
在C语言中,使用宏定义来解决函数建立过多且函数内容较少的问题。但是宏在使用过程中也存在着也许优点与缺点:
宏函数的优点:不需要建立栈帧,提高调用效率,增强代码的复用性
宏函数的缺点:复杂、容易出错;可读性差;不能调试。
在C++中,会使用内联函数来代替部分宏函数。
以inline修饰的函数被称为内联函数,编译时C++编译器会根据情况在调用内联函数的地方进行展开,没有函数调用建立栈帧的开销,内联函数提高程序运行的速度。
//内联函数 inline int Add(int x, int y) { return (x + y) * 10; } int main(void) { for (int i = 0; i < 100; i++) { cout << Add(i, i + 1) << endl; } return 0; }
由此可知,宏函数与内联函数是很相似的,但C++对宏函数进行了优化,宏函数与内联函数适用于短小,需要频繁调用的函数。
但是并不是所有的函数都可以使用内联函数,否则就会导致可执行程序变大。
这里假设Func不是内联函数(不被inline修饰),每次执行Func函数时都会跳转到Func去执行,如果存在n个位置调用Func函数,则合计会有m+n条指令;
这里假设Func是内联函数(被inline修饰),相当于每次执行Func函数都会对Func进行展开,如果存在n个位置调用Func函数,则合计会有m*n条指令。
若调用Func函数过多,则会导致可执行程序变大。
编译器在识别被inline修饰的内联函数时,内联函数对编译器只是一个建议,最终时候成为内联函数,编译器会自己决定,在这些情况下,编译器会自动否决内联:1.比较长的函数;2.递归函数。
默认在debug版本下,inline不会起作用,否则无法支持调试。在debug版本下,需要对编译器进行设置:
这里可以发现,在调用代码较少时,此时内联函数起效果;
inline int Add(int x, int y) { for (int i = 0; i < 100; i++) { x *= 2; } for (int i = 0; i < 100; i++) { x /= 2; } for (int i = 0; i < 100; i++) { y *= 2; } for (int i = 0; i < 100; i++) { y /= 2; } return x + y; } int main(void) { int ret = Add(1, 2); return 0; }
此时,函数内容过多,内联函数被编译器否决;
inline int Add(int x,int y) { if (x > 5 && y > 5) return x + y; return Add(x + 1, y + 1); } int main(void) { int ret = Add(1, 2); cout << ret << endl; return 0; }
此时,函数递归,内联函数被编译器否决;
//Func.h文件 inline int Add(int x, int y); //Func.cpp文件 #include"Func.h" inline int Add(int x, int y) { return (x + y) * 10; } //test.cpp文件 #include<iostream> #include"Func.h" using namespace std; int main(void) { int ret = Add(1, 2); cout << ret << endl; return 0; }
如果内联函数没有被编译器否决,那么内联函数就会在编译期间会被展开,建议内联函数声明与定义不分离,直接写在头文件中。
原因是,内联函数不会被call,所有内联函数就不会进入符号表进行链接,如果声明与定义分离,会导致链接错误,内联函数被展开后寻找不到函数地址,链接就会找不到。