关于C++是什么
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
C++在许多领域都有广泛的应用,包括游戏开发、系统/应用软件、驱动和嵌入式系统开发、高性能服务器和客户端应用,以及创建大型复杂的图形用户界面和数据库应用。
一、关键字
C语言中的关键字有32个,C++中的关键字有63个,几乎增长了一倍。
asm | do | if | return | try | continue | auto | double | inline |
---|---|---|---|---|---|---|---|---|
short | typedef | for | bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw | case | enum | mutable |
static | union | wchar_t | catch | namespace | explicit | static_cast | unsigned | default |
char | export | new | struct | using | friend | class | extern | operator |
switch | virtual | register | const | false | private | template | void | true |
while | protected | this | volatile | const_cast | delete | template | goto | reinterpret_cast |
二、命名空间
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
2.1 命名空间的作用
命名空间能够避免命名冲突。当模块越来越大时,不同模块中可能存在相同名称的类、函数等。使用命名空间可以将相关的类和函数分组,有效避免这些名称冲突。
例子:在下面这个代码中就会发生命名冲突,原因是因为全局中定义了一个rand
变量,而头文件#include <stdlib.h>
中包含了rand函数
的声明,这样就导致了命名冲突,最终编译报错。
#include <stdio.h>
#include <stdlib.h>//命名冲突
int rand = 0;int main()
{printf("%d", rand);return 0;
}
2.2 命名空间定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}
即可,{}
中即为命名空间的成员。
下面代码就展示了如何定义命名空间,关键字 + 命名空间的名字 ,aj 是我CSDN的名字,这里就用他来做我命名空间的名字,而命名空间的名字是可以按照自己的想法自由定义。
namespace aj //我CSDN的名字
{int rand = 0;
}
命名空间的成员不仅仅可以是内置类型、自定义类型,还可以是函数。
namespace aj //我CSDN的名字
{//内置类型int rand = 0;//函数void Swap(int* e1, int* e2){int tmp = *e1;*e1 = *e2;*e2 = tmp;}//自定义类型struct SeqList{int* a;int size;int capacity;};
}
命名空间能够嵌套使用
namespace aj //我CSDN的名字
{//内置类型int rand = 0;//函数void Swap(int* e1, int* e2){int tmp = *e1;*e1 = *e2;*e2 = tmp;}//自定义类型struct SeqList{int* a;int size;int capacity;};namespace aj1{int rand = 1;int Add(int x1, int x2){return x1 + x2;}}}
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace aj //我CSDN的名字
{int i = 0;
}namespace aj //我CSDN的名字
{double d = 0;
}/*namespace aj //我CSDN的名字
{int i = 0;double d = 0;
}*/
在这里可以试验出两个相同名称的命名空间编译并不会报错,前面两个命名空间会合并成第三个命名空间。
2.3 命名空间的使用
定义一个命名空间就相当于产生了一个新的作用域,在全局中并不能直接使用命名空间中的成员。
使用命名空间中成员的方法一共有三种:
- 加命名空间名称及作用域限定符 ( :: 作用域限定符)
- 用 using 将命名空间内某个成员引入 ( using + 命名空间名称 :: 成员名)
- 使用 using namespace 加命名空间名称引入
- 加命名空间名称及作用域限定符 ( :: 作用域限定符)
namespace aj //我CSDN的名字
{//内置类型int rand = 0;
}int main()
{printf("%d\n", aj::rand); //命名空间中的randprintf("%p\n", rand); // #inlcude <stdlib.h> 中 rand 函数地址return 0;
}
- 用 using 将命名空间内某个成员引入 (using + 命名空间名称 :: 成员名)
在下面的代码中,我使用 (using + 命名空间名称 :: 成员名 )的方法将命名空间中的成员i
引入,那么成员 i
就能够在全局中被使用,而 d
未被引入,使用 d
就会报错。
namespace aj //我CSDN的名字
{//内置类型int i = 0;double d = 0.0;
}using aj::i;int main()
{printf("%d", i);printf("%lf", d);return 0;
}
注意:如果使用 using + 命名空间名称 :: 成员名导入的成员与函数名相同,会产生名称冲突。
- 使用 using namespace 加命名空间名称引入
namespace aj //我CSDN的名字
{//内置类型int i = 0;double d = 0.0;
}using namespace aj;int main()
{printf("%d", i);printf("%lf", d);return 0;
}
using namespace 加命名空间名称
与普通using
作用相同,但前者是对整个命名空间作用,把该命名空间下的所有类、函数、常量等均导入当前作用域。
注意:若将整个命名空间中的所有成员全部导入当前作用域,那么就相当于这个命名空间失效,则有没有这个命名空间都相同,所以说这种方法不是很推荐,推荐前两种方法。
三、C++ 的输入、输出
-
C++中使用
cout 标准输出对象
和cin 标准输入对象
时,必须包含#include <iostream >头文件
以及按命名空间使用方法使用std
。cout --> console 控制台 + out cout 表示标准输出,它输出到控制台。
cin --> console 控制台 + in cin 表示标准输入,它从键盘接收输入。
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std ;int main()
{cout << "Hello C++" << endl; return 0;
}
-
endl
( end of line ) 是特殊的C++符号,表示换行输出 ,与'\n'
的作用相同。endl
与 cin、cout 都需要包#include <iostream>
才能使用。
-
<<是流插入运算符,>>是流提取运算符。
- 相比于C语言中的输入输出,C++输入输出更方便,C++的输入输出可以自动识别变量类型,C语言中的输入输出需要自己判断输出变量的类型。
#include<iostream>
using namespace std ;int main()
{int num1 = 0;int num2 = 0;//C语言的输入输出scanf("%d", &num1);printf("%d\n", num1);//C++的输入输出cin >> num2;cout << num2 << endl;return 0;
}
注意:std 是C++标准库的命名空间
在我们日常敲代码的时候,代码量不会很大,只需要 using namespace std
展开命名空间。
而当我们做项目的时候代码量很大,这样做会将C++的标准库全部展开,会很容易产生命名冲突 , 所以建议前面提到的第一种 (std::cin)
指定命名空间成员和第二种方法(using std::cin)
展开常用函数 / 类型。
四、缺省函数
4.1 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
#include<iostream>
using namespace std;void Print(int x = 0)
{cout << x << endl;
}int main()
{Print();Print(520);Print(1314);return 0;
}
4.2 缺省函数分类
4.2.1. 全缺省函数
全缺省函数是在定义函数的时候所有参数都有默认值
#include<iostream>
using namespace std;void Print(int x = 1, int y = 2, int z = 3)
{cout << x << endl;cout << y << endl;cout << z << endl;
}int main()
{Print();Print(10);Print(10, 20);Print(10 , 20 ,30);return 0;
}
4.2.2. 半缺省函数(部分缺省函数)
半缺省函数是在定义函数的时候部分参数有默认值
#include<iostream>
using namespace std;void Print(int x , int y = 2, int z = 3)
{cout << x << endl;cout << y << endl;cout << z << endl;cout << endl;}int main()
{Print(10);Print(10, 20);Print(10, 20, 30);return 0;
}
注意:
- 半缺省函数的指定参数必须从右向左给
//错误示范
void Print(int x = 1 , int y , int z = 3)
{cout << x << endl;cout << y << endl;cout << z << endl;cout << endl;
}
- 调用缺省函数时,传参必须从左向右传,不能指定传参。
void Print(int x = 1, int y = 2, int z = 3)
{cout << x << endl;cout << y << endl;cout << z << endl;cout << endl;}int main()
{Print(, 20 ,);Print(, , 30);return 0;
}
例如我只想给第二个位置传一个值,或是只给第三个位置传一个值,这样传参是不被允许的。
- 缺省参数不能在函数声明和定义中同时出现
// test1.h
void Func(int a = 10);
// test1.cpp
void Func(int a = 20){}
//注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
4.3 使用缺省值的好处
namespace aj
{typedef struct Stack{int* _a;int _top;int _capacity;}ST;void StackInit(ST* st){st->_a = NULL;st->_capacity = 0;st->_top = -1;}
}
在初始化栈的时候,有的人会先开一部分空间,有的人会选择先不给空间,但是都不可避免的是,如果入栈个数很多的情况下会进行大量的扩容行为,扩容也是有消耗的,所以不提倡大量的扩容行为。那么有的人会说,初始化的时候开一个大空间,那么就会有下面三个问题
- 首先开多少空间叫做大空间
- 其次是入栈次数的情况下会浪费空间
- 最后是我们定义多个栈且知道需要的栈容量不同的时候,每个栈的初始空间都相同显然是不合理的。
下面这个代码就能知道缺省值能够起多么大的作用。
namespace aj
{typedef struct Stack{int* _a;int _top;int _capacity;}ST;// 缺省值为4,默认开四个int大小空间// 若显示传参,那么则开参数个int大小的空间void StackInit(ST* st, int N = 4){st->_a = (int*)malloc(sizeof(int) * N);st->_capacity = N;st->_top = 0;}void StackPush(ST* st, int x){// ...// 入栈操作在栈中有详细讲解,这里省略}
};int main()
{// 需要入栈5个数据aj::ST st1;aj::StackInit(&st1, 5);for (int i = 0; i < 5; i++){aj::StackPush(&st1, i);}// 需要入栈500个数据aj::ST st2;aj::StackInit(&st2, 500);for (int i = 0; i < 500; i++){aj::StackPush(&st2, i);}// 不知道需要入栈多少个数据,默认申请四个空间aj::ST st3;aj::StackInit(&st3);return 0;
}
五、函数重载
5.1 函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
- 同名函数的形参列表(参数个数或类型或类型顺序)不同
// 参数类型不同
void func(int x, int y)
{// ...
}void func(double x, double y)
{// ...
}// 参数顺序不同
void func(char x, int y)
{// ...
}void func(int x, char y)
{// ...
}// 参数个数不同
void func(char x, char y)
{// ...
}void func(char x, char y, char z)
{// ...
}
- C++允许在同一作用域中声明几个功能类似的同名函数
下面的两个函数是否是函数重载呢?显然不是,这两个函数根本不在同一个作用域中。
namespace aj1
{void func(int x, int y){// ...}
}namespace aj2
{void func(double x, double y){// ...}
}
在不同的作用域中,就算函数一模一样也不构成函数重载。
namespace aj1
{void func(int x, int y){// ...}
}namespace aj2
{void func(int x, int y){// ...}
}
那么下面两个函数构成函数重载吗?构成,原因是命名空间的名字相同,内容会合并,两个函数的参数不同,即构成函数重载。
namespace aj1
{void func(int x, int y){// ...}
}namespace aj1
{void func(double x, double y){// ...}
}
函数重载这个语法能够带来很多便捷。例如Swap函数 、 Add函数等等。
实现同种功能,但参数类型不同的时候非常好用,使用的时候像是使用同一个函数,但实际上不是。
// C语言的写法
void Swapi(int* e1, int* e2)
{int tmp = *e1;*e1 = *e2;*e2 = tmp;
}void Swapd(double* e1, double* e2)
{double tmp = *e1;*e1 = *e2;*e2 = tmp;
}// C++写法
void Swap(int* e1, int* e2)
{int tmp = *e1;*e1 = *e2;*e2 = tmp;
}void Swap(double* e1, double* e2)
{double tmp = *e1;*e1 = *e2;*e2 = tmp;
}
5.2 C++支持函数重载的原理–名字修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持函数重载
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
Test.cpp
预处理 头文件展开、宏替换、删除注释、条件编译Test.cpp 通过 预处理 生成 Test.i
编译 检查语法、生成汇编指令Test.i 通过 编译 生成 Test.s
汇编 将汇编代码转换为二进制机器码Test.s 通过 汇编 生成 Test.o
链接 合并链接,生成可执行程序Test.o 通过 链接 生成 a.out / Test.exe
下面用VS来测试一下编译后C语言、C++的函数会被修饰成什么。
C++中:
void func(int x, double y);
//{
// cout << "func(int x, double y)" << endl;
//}void func(double x, int y);
//{
// cout << "func(double x, int y)" << endl;
//}int main()
{func(2, 2.2);func(2.2, 2);return 0;
}
解释一下为什么这里会报错:
由于函数的地址实际上是函数第一句指令的地址,那么函数只有声明,那么这个函数就不会进入符号表中,当调用这个函数的时候通过修饰后的名字在符号表中寻找函数的地址,找不到,就报错。
C语言中:
void func(int x, double y);
//{
// //cout << "func(int x, double y)" << endl;
//}int main()
{func(2, 2.2);return 0;
}
很明显C++中同名函数的参数不同,那么修饰出来的名字也不同,而C语言中并没有函数名修饰,这里的函数调用只是在函数前面加了一个_ , 这样就能知道为什么C语言中不允许出现同名函数(也就是函数重载),而C++中可以支持。
由于VS下的函数名修饰规则过于复杂,接下来使用Linux看一下gcc编译器下的函数名修饰规则。
void func(double x, int y)
{cout << "func(double x, int y)" << endl;
}int main()
{func(2.2, 2);return 0;
}
void func(int x, double y)
{cout << "func(int x, double y)" << endl;
}void func(double x, int y)
{cout << "func(double x, int y)" << endl;
}int main()
{func(2, 2.2);func(2.2, 2);return 0;
}
通过上面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
并且上面的图片也能证明函数的地址是函数第一条指令的地址。
g++的函数名修饰规则是【_Z+函数长度+函数名+类型首字母】,里面并不没有与返回值相关的内容,所以返回值不同不能构成函数重载。
如果说允许定义的时候,函数参数相同,返回值不同,那么能构成函数重载吗?不行,看下面两个函数,调用函数时并不知道会调用哪个,存在调用歧义。所以说记住返回值不同不能构成函数重载。
void func(int x, double y)
{cout << "func(int x, double y)" << endl;
}int func(int x, double y)
{cout << "func(int x, double y)" << endl;
}int main()
{func(2, 2.2);return 0;
}
六、 引用
6.1 引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
比如:周树人,他的笔名是鲁迅,但是周树人和鲁迅是同一个人
类型& 引用变量名(对象名) = 引用实体;
int main()
{int a = 0;int& ra = a;cout << &a << endl;cout << &ra << endl;return 0;
}
注意:** 引用类型必须和引用实体是同种类型**的
6.2 引用特性
- 引用在定义时必须初始化
int main()
{int& a;return 0;
}
- 一个变量可以有多个引用
int main()
{int a = 0;int& b = a;int& c = a;return 0;
}
- 引用一旦引用一个实体,再不能引用其他实体
int main()
{int a = 0;int& ra = a;int m = 1;ra = m;return 0;
}
6.3 常引用
注意:
- 当值返回、类型提升、类型截断都会先将值赋给临时变量,而临时变量具有常属性(不能被改变)。
- 权限可以缩小、可以平移不可以放大
int main()
{const int a = 0;// int& b = a; // 错误,a有const限制,所以b必须有const限制,这里是权限放大// int b = a, // 正确,赋值不受限制const int x = 0;const int& y = x; // 正确,这里是权限的平移int m = 0;const int& n = m; // 正确,这里是权限的缩小int i = 0;double d = i; // 正确,赋值不受限制// double& ri = i; // 错误,类型提升,生成临时变量,具有常属性const double& ri = i; return 0;
}
6.4 使用场景
- 做参数
void Swap(int& e1, int& e2)
{int tmp = e1;e1 = e2;e2 = tmp;
}
以前写Swap函数的时候需要将两个变量的地址传过去,再解引用,现在引用实现了形参的改变影响实参。
在C语言中,有部分题目会出现参数做返回值的,也叫输出型参数,以前需要传地址,现在只需要传引用。
int BtreeSize(struct TreeNode* root)
{return root == NULL ? 0 : BtreeSize(root->left) + BtreeSize(root->right) + 1;
}void _preorderTraversal(struct TreeNode* root, int* arr , int* pi)
{if(root == NULL)return;arr[(*pi)++] = root->val;_preorderTraversal(root->left, arr , pi);_preorderTraversal(root->right, arr , pi);
}int* preorderTraversal(struct TreeNode* root, int* returnSize){*returnSize = BtreeSize(root); //遍历二叉树,记录节点个数int* arr = (int*)malloc(*returnSize*sizeof(int));int i = 0;_preorderTraversal(root, arr , &i);//用子函数递归遍历字符串return arr;
}
例如上面这个代码中的 *returnSize
就是输出型参数,可以使用引用。在子函数中我们希望 i
都是同一个 i
, 所以传地址,但是使用引用改造一下会更加符合我们的使用习惯。
int BtreeSize(struct TreeNode* root)
{return root == NULL ? 0 : BtreeSize(root->left) + BtreeSize(root->right) + 1;
}void _preorderTraversal(struct TreeNode* root, int* arr , int& ri)
{if(root == NULL)return;arr[ri++] = root->val;_preorderTraversal(root->left, arr , ri);_preorderTraversal(root->right, arr , ri);
}int* preorderTraversal(struct TreeNode* root, int& returnSize){returnSize = BtreeSize(root); //遍历二叉树,记录节点个数int* arr = (int*)malloc(*returnSize*sizeof(int));int i = 0;_preorderTraversal(root, arr , i);//用子函数递归遍历字符串return arr;
}
- 做返回值
int& count()
{static int n = 0; n++;return n;
}
注意: 如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
6.5 传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
接下来使用在网上找来的两段代码,来分别测试传值和传引用的效率。
- 测试的当参数分别是是传值和传引用的效率
#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;
}
- 测试的当返回值分别是是传值和传引用的效率
#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;
}
通过上面两段代码可以发现,传值和传引用的效率明显是传引用的效率更高。
6.6 指针和引用的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{int a = 0;int* pa = &a;int& ra = a;return 0;
}
int main()
{int a = 0;int* pa = &a;int& ra = a;(*pa)++;ra++;return 0;
}
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
七、 内联函数
7.1 内联函数的概念
以inline
修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开**,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
inline void Swap(int& e1, int& e2)
{int tmp = e1;e1 = e2;e2 = tmp;
}int main()
{int a = 0, b = 1;Swap(a , b);return 0;
}
从上面两张图片中我们能看到,内联函数调用的时候并不是call
跳转到函数上,而是替换。
如果在上述函数前增加inline
关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
7.2 内联函数的特性
inline
是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。inline
对于编译器而言只是一个建议,不同编译器关于inline
实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline
修饰,否则编译器会忽略inline
特性。
内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。inline
不建议声明和定义分离,分离会导致链接错误。因为inline
被展开,就没有函数地址了,链接就会找不到。
// test1.h
#include <iostream>
using namespace std;
inline void func(int x);// test1.cpp
#include "test1.h"
void func(int x)
{cout << x << endl;
}// test.cpp
#include "test1.h"int main()
{func(520);return 0;
}
原因: 当函数为内联函数时调用函数时不会直接跳转到函数内,而是将调用语句替换成函数,那么在编译过程中认为没有必要形成指令,在汇编过程中,函数名也不会进入符号表,在 test.cpp文件
中展开 test1.h 文件
,而 test.h文件
中只有声明,在调用过程中没有内容可以替换,那么就会去调用函数,而在符号表中又找不到这个函数,那么就会报错。
7.3 设计内联函数的目的
设计内联函数的目的是用来替换宏函数。
- 宏函数的缺点
(1)容易出错,语法坑很多
(2)不能调试
(3)没有类型的安全检查 - 宏函数的优点
(1) 没有严格的类型检查
(2)当频繁调用一个小函数时,将函数写成宏函数,调用函数时不会建立栈帧,提高效率
下面写几种常见 Add宏函数 来说明为什么宏函数容易出错。
//#define Add(a , b) a+b // (1)
//#define Add(a , b) a+b; // 错误 , 宏定义时后面不能加分号
//#define Add(a , b) (a)+(b) // (2)
//#define Add(a , b) ((a)+(b)) // (3)int Add(int a, int b) // (4)
{return a + b;
}int main()
{int a = 3;int b = 5;int mul = Add(a|b, a&b) * 7;// (1) => a | b + a & b * 7 => 3 | 5 + 3 & 5 * 7// 3 | 5 + 3 & 8 => 3 | 8 & 35 => 1 & 35 => 1// (2) => (a) | (b) + (a) & (b) * 7 => (3 | 5) + (3 & 5) * 7// 1 + 1 * 7 => 1 + 7 => 8// (3) => ((a) | (b) + (a) & (b)) * 7 => ((3 | 5) + (3 & 5)) * 7// (1 + 1) * 7 => 2 * 7 => 14// (4) => 2 * 7 => 14return 0;
}
上述宏函数定义完全正确的只有(3),调用宏函数时,无论参数是数字还是表达式都是直接替换,操作符的优先级会影响表达式的结果,而当我们写函数的时候,基本上不会出现这类问题,因为函数传参时会将表达式计算后传过去。
设计出内联函数在调用的地方直接展开可以直接替换掉宏函数,而后面学习的模版能够解决类型的问题。
除了宏函数,还有宏常量,可以用 const
和 enum
替换宏常量。
八、auto关键字(C++11)
8.1类型别名思考
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
- 类型难于拼写
- 含义不明确导致容易出错
auto
能够通过右边的值,自动推算出左边的类型。
#include <iostream>
using namespace std;#include <vector>
#include <string>int main()
{int a = 0;auto b = a;auto c = &a;auto& d = a;// 普通场景没有什么价值// 类型很长,就有价值,简化代码std::vector<std::string> v;std::vector<std::string>::iterator it = v.begin();//auto it = v.begin();cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;cout << typeid(it).name() << endl;return 0;
}
std::map<std::string, std::string>::iterator
是一个类型,但是该类型太长了,特别容易写错。
auto
是作为一个新的类型指示符来指示编译器,auto
声明的变量必须由编译器在编译时期推导而得。
注意: 使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型。因此 auto 并非是一种"类型"的声明,而是一个类型声明时的"占位符",编译器在编译期会将 auto 替换为变量实际的类型。
8.2 auto的使用细则
- auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{int a = 0;auto b = &a;auto* c = &a;auto& d = a;cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{auto m = 520, n = 521;//auto x = 520, y = 521.0;// 这里编译会出错,因为x,y的初始化类型不同return 0;
}
8.3 auto不能推导的场景
- auto 不能作为函数的参数
//此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void func(auto i)
{//
}
- auto 不能直接用来声明数组
int main()
{auto arr[] = { 1 , 2 , 3 };return 0;
}
九、 基于范围的for循环(C++11)
9.1 范围for的语法
在以前没有范围 for 的时候,下面的代码就是我们常见遍历数组的方式
void func()
{int arr[] = { 1, 2, 3, 4, 5 };for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){arr[i] *= 2;}for (int* p = arr; p < arr + sizeof(arr) / sizeof(arr[0]); p++){cout << *p << endl;}
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此后面引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void func()
{int arr[] = { 1, 2, 3, 4, 5 };for (auto& e : arr){e *= 2;}for (auto e : arr){cout << e << " ";}
}
9.2 范围for的使用条件
for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意: 以下代码就有问题,因为for的范围不确定
void func(int arr[])
{for (auto& e : arr){cout << e << endl;}
}
原因是范围 for 使用的参数必须是数组名,如果将数组名作为函数参数传递给函数,那么在函数内部这个参数被看作是一个指针,而不是数组,所以说这个代码有问题。
十. 指针空值nullptr(C++11)
nullptr
的出现实际上是为了补救 C++ 98 中 NULL
的坑。
NULL
实际是一个宏,在传统的C头文件(stddef.h)
中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
对于学习C语言的人来说, NULL 就是被当做空指针使用,而 C++ 中却把 NULL 定义成 0
,与我们学习的C语言相悖,但是因为有些程序员就是把NULL当做0来使用,也不能将NULL的内容直接改变,所以nullptr
就出现了。
注意:
1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2.在C++11中,sizeof(nullptr)
与sizeof((void*)0)
所占的字节数相同。
3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
结尾
本次的内容就到此为止了,对于今天的内容,对于一个萌新来说会有一点难理解,但是一定要对这一部分非常熟悉,这部分内容会在后面的知识中反复的被使用。
C++的出现是因为 本贾尼·斯特劳斯特卢普(Bjarne Stroustrup 原来以C语言为主要编程语言,但他觉得C语言在一些面向对象的特性和容易出错的问题上还不完善,需要改进。这就动机他基于C语言研发出C++,弥补C语言的短板,成为当时更适合软件开发的语言。
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
这一篇万字C++入门也是写了一段时间,如果这篇文章对你有用的话,请大家给一个三连支持一下!!🌹🌹