一文掌握C++基本语法
一文掌握C++基本语法
本文将带你全面掌握C++的基本语法,从关键字、命名空间到输入输出、缺省参数、函数重载、引用、内联函数、auto关键字、基于范围的for循环以及指针空值nullptr等核心概念,适合C++初学者深入学习。
1. C++关键字
C++关键字是C++语言预定义的保留字,它们在程序中有特殊的含义,不能用作标识符(如变量名、函数名等)。C++的关键字包括但不限于:auto
、bool
、break
、case
、catch
、char
、class
、const
、const_cast
、continue
、default
、delete
、do
、double
、dynamic_cast
、else
、enum
、explicit
、export
、extern
、false
、float
、for
、friend
、goto
、if
、inline
、int
、long
、mutable
、namespace
、new
、operator
、private
、protected
、public
、register
、reinterpret_cast
、return
、short
、signed
、sizeof
、static
、static_cast
、struct
、switch
、template
、this
、throw
、true
、try
、typedef
、typeid
、typename
、union
、unsigned
、using
、virtual
、void
、volatile
、wchar_t
、while
。
2. 命名空间
变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace
关键字的出现就是针对这种问题的。
2.1 命名空间定义
使用namespace
关键字,后面跟命名空间的名字,然后用{}
将成员括起来即可,和C语言的结构体类似。存在多个相同的命名空间的时候,编译器编译的时候会把它们合并,如下面:
namespace myNamespace {
int x = 5;
int y = 10;
}
命名空间可以嵌套:
namespace outer {
namespace inner {
int z = 15;
}
}
PS:命名空间定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中,但是成员的生命周期没有改变,仍然是全局的。
2.2 命名空间使用
命名空间有3种使用方式:
- 加命名空间名称及作用域限定符
这种方法每次使用的时候都要进行::
限定,比较麻烦:
std::cout << "Hello, World!" << std::endl;
- 使用
using
将命名空间中成员引入
这样就可以不用::
限定命名空间,直接使用b
:
using std::cout;
using std::endl;
cout << "Hello, World!" << endl;
- 使用
using namespace
命名空间名称引入
可以使用所有N中内容,但是需要注意的是这样可能会造成名称冲突:
using namespace std;
cout << "Hello, World!" << endl;
3. C++输入&输出
- 使用
cout
标准输出(控制台)和cin
标准输入(键盘)时,必须包含<iostream>
头文件以及std
标准命名空间。 - 使用C++输入输出更方便,不需要增加数据格式控制。
4. 缺省参数
缺省参数就像女神的备胎,当女神有男朋友的时候,备胎被冷落在一旁,当女神分手后,备胎才可以派上用场。
4.1 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
4.2 缺省参数分类
- 全缺省参数
- 半缺省参数
注意:
- 半缺省参数必须从右往左依次给出,不能间隔着给
- 缺省参数不能在函数声明和定义中同时出现,且应只出现于声明
- 缺省值必须是常量或者全局变量
5. 函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。比如:有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”
5.1 函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
下面都是函数重载:
void print(int x);
void print(double x);
void print(char* x);
下面这两个不是函数重载!
5.2 名字修饰
那么为什么C++支持函数重载而C语言不支持呢?一个程序要运行起来要经历以下几步:假设有以下文件
- f.h
- f.cpp
- test.cpp
预处理——头文件展开,宏替换,条件编译,去掉注释
生成f.i test.i编译——检查语法,生成汇编代码
生成f.s test.s汇编——把汇编代码转换成二进制的机器码(让CPU能看懂)
生成f.o test.o链接——找调用函数的定义地址,链接对应上,合并到一起
生成a.out可执行程序
在链接阶段,编译器看到test.o调用了哪些函数,就会去f.o的符号表中找对应函数的地址,然后链接在一起,那么要怎么去找呢?C++和C语言采用了不同的名字修饰,就会用不同的名字去找
我们发现名字由_Z+函数名长度+函数名首字母+函数按顺序参数首字母
,因此支持函数重载(名字不一样,找的对象就不一样)
C:
这里我们发现C语言则是直接用函数名作为地址查找对象,回想我们的函数指针,是不是函数指针和函数名都可以调用函数呢。因此如果C语言中出现函数重载,那么调用的时候就会查找到两个不同地址,C语言不知道要链接哪一个,因此出错。
5.3 extern "C"
我们知道C语言可以调用C语言的静态库和动态库,C++可以调用C++的静态库和动态库,那么C++能不能调用C的库?C能不能调用C++的库?
答案是可以的。
我们先来看C++调用C的库,这里我们用C语言实现的栈的代码生成一个静态库
再新开一个项目,调用这个库,这里对新项目的属性进行更改。
然后来看一下现在能否调用
这是不是意味着C++不能调用C的库?别急,这时候extern C就登场了
我们发现加了这个关键字后就链接成功了。这个关键字的意思是告诉编译器将该部分按照C语言的规则来进行编译,我们调用C库,当然就要按C的规则来啦。
再来看一下C语言能否调用C++的库,我们把后缀名更改一下
(这里是c语言了,因此只能用typedef过的别名,不能再用一个Stack,因为C语言不支持) 好像不行,我们刚刚是在C++的部分进行修改,这次我们也在C++的部分进行修改,我们来到test_01,对头文件部分的声明进行如下修改。
这样是不是就顺利链接成功了,这里的预编译进行了判断,如果文件是C++的,那么就用C的规则进行编译,也可以这么写:
6. 引用
6.1 引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
引用类型必须和引用实体是同种类型的
6.2 引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
6.3 常引用
这里我们取别名的规则是:
对原引用变量,权限只能缩小,不能放大。这里权限指的是读写权限。
因此对const常量只能进行读,也就只能用const引用。
在下面将double赋给int别名的过程中,编译器会产生“临时变量”,将double d中整数部分赋给临时变量, 这个临时变量再将这个整数赋给另一个int类型的临时变量,最后int把临时变量赋给rd,也就是说rd引用的其实是这个int临时变量,而临时变量具有常属性,因此要用const引用。
6.4 使用场景
- 作函数参数
在学习C语言的时候我们如果要交换两个数,那么就需要传址调用函数,而现在可以直接用引用进行修改,这就是引用做参数的好处,输出型参数 另外,如果是传值调用的话,我们知道形参是实参的一个临时拷贝,而传引用的时候并没有拷贝这一步,这就提高了效率。
- 作函数返回值
结果是显然的,我们返回的是静态变量n的别名,而出了函数作用域n并没有销毁,那么来看下面这个代码
n和ret地址一样
这里n不再是静态变量了,出了函数作用域后空间返还,而我们的ret是n的别名,这是不是就相当于野指针。因此只有第一次调用函数后ret中的值是1,调用一次函数(函数重载<<)后n的空间被制成随机数,因此剩下两次都是随机数,因此我们用引用作返回值的时候返回的应该是出了函数定义域还存在的变量的引用,否则就应该按值返回,这里和指针是一个道理。
明白了这些下面这个程序的结果就应该很明确了,应该是7
道理是一样的。另外引用作返回值也会提高效率
6.5 传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
这里用程序来比较一下
不是很明显的原因是我们计算机的速度实在太快,但是还是有效率差异的。指针和引用的效率是一样的,因为引用的底层和指针的底层是一样的!
6.6 引用和指针的区别
从语法概念上:引用是别名,没有额外开空间,而指针存储变量的地址,开辟了4/8字节的空间
从底层实现的角度:引用实际上是有空间的,因为引用是按指针的方式来实现的
来看一下指针和引用的汇编
汇编指令完全一样,也印证了底层是一样的
那引用和指针的不同点是什么呢?
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
7. 内联函数
7.1 概念
以inline
修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
在学习C语言的时候,我们遇到函数体短小且频繁调用的函数的时候,采用的优化方式是使用宏进行替换,但是宏太繁琐啦,难以看懂,因此c++就产生了内联函数。
那么内联函数是如何进行优化的呢?来看代码。
当我们不用inline
定义函数的时候,函数是怎么调用的呢?
进行函数压栈,然后用call
指令调用。那么用了inline
后呢
我们发现在汇编代码中直接对函数调用的代码进行了替换,没有压栈和call
调用指令。
7.1 特性
inline
是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。inline
对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline
的函数体内有循环/递归等等,编译器优化时会忽略掉内联。inline
不建议声明和定义分离,分离会导致链接错误。因为inline
被展开,就没有函数地址了,链接就会找不到。
例如如果在.h和.cpp文件中分别声明和定义内联函数,那么在链接的时候就会出现错误:
main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
那么内联函数相比宏有哪些优点?
- 内联函数在debug版本下支持调试,而宏不支持
- 内联函数就是普通函数的写法,解决了宏晦涩难懂的问题
8. auto关键字(C++11)
8.1 auto简介
在早期C/C++中auto
的含义是:使用auto
修饰的变量,是具有自动存储器的局部变量
C++11中,标准委员会赋予了auto
全新的含义即:auto
不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto
声明的变量必须由编译器在编译时期推导而得。
使用auto
定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto
的实际类型。因此auto
并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto
替换为变量实际的类型。
8.2 auto的使用细则
- 用
auto
声明指针类型时,用auto
和auto*
没有任何区别,但用auto
声明引用类型时则必须加&
- 在同一行定义多个变量 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
8.3 auto不能推导的场景
auto
不能作为函数的参数auto
不能直接用来声明数组- 为了避免与C++98中的
auto
发生混淆,C++11只保留了auto
作为类型指示符的用法 auto
在实际中最常见的优势用法就是C++11提供的新式for循环,还有lambda表达式等进行配合使用。
9. 基于范围的for循环(C++11)
9.1 范围for的语法
for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
范围for循环与普通循环类似,可以用continue
来结束本次循环,也可以用break
来跳出整个循环。
9.2 范围for的使用条件
- for循环迭代的范围必须是确定的对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供
begin
和end
的方法,begin
和end
就是for循环迭代的范围。 - 迭代的对象要实现
++
和==
的操作。
10. 指针空值nullptr(C++11)
10.1 C++98中的指针空值
在传统头文件中有如上定义,我们发现在C++中Null
被宏替换为0,而如果我们要对一个指针赋空值,仍采用NULL
的话有些不妥,因为0毕竟是一个数字,而不是指针类型,因此C++11引入了nullptr
,其实也就相当于(void*)0
。