简介
本文将介绍c++相关基础知识,包括关键字、命名空间、c++输入输出、缺省参数、函数重载、引用、内联函数、auto关键字、基于范围的 for 循环、指针空值—nullptr等知识点。
正文
首先我们要知道 c++ 和 c 语言的区别,两者都是高级语言。
而 c语言是面向过程,是基于函数编程的;c++是基于面向对象的,它是对 c语言中的一些不合理的地方进行了改进。
那么现在开始了解一些 c++ 的基础知识:
- 关键字
c++关键字的个数和版本有关系,c++98版本有63个关键字,而 c语言的 c99版本有32个关键字。
下表列出了c++98版本的各个关键字:
- 命名空间
因为在 c++中,变量、函数、类都是大量存在的,这些变量、函数、类都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的就是对标识符的名称进行本地化,以避免命名冲突(名字污染)。命名空间就是为了解决名字冲突(名字污染)的问题的。
那么命名空间是如何定义的呢?首先要有 namespace 这个关键字。命名空间有两种表示方式:- 普通命名空间
namespace 命名空间的名字 { 成员列表; } 例如:namespace N1 { int a = 10; int b = 20; int Add(int left, int right) { return left + right; } }
命名空间可以放变量名,也可以放函数,结构体
- 嵌套
namespace N2
{
int a = 10;
int b = 20;
int Sub(int left, int right){
return left - right;
}
namespace N3
{
int a = 10;
int b = 20;
int Mul(int left, int right){
return left * right;
}
}
}
此处要注意的是在同一个工程中可以定义多个相同的命名空间,但是编译器会自行将多个相同名称的命名空间合并成一个
例如:
namespace N1
{
int a = 10;
int b = 20;
int Add(int left, int right)
{
return left + right;
}
}
namespace N1
{
int c = 0;
int d = 0;
int Div(int left, int right)
{
if (0 == right)
exit(1);
return left / right;
}
}
//此时N1::会有a,b,Add,c,d,Div六个成员,即合并了两个N1命名空间。
命名空间的使用方法:
方式一:命名空间::成员名字(::是作用域限定符)
方式二:using 命名空间名字::成员名字; //相当于把这个命名空间的名字导出来作为当前工程的全局变量
例:using N1::a;
方式二的适用场景:当前命名空间中个别成员多次在某个文件中被使用,但是可能会造成命名冲突,因为当前工程可能本来就有一个全局变量。
方式三:using namespace N1; //将整个命名空间导出来
方式三的适用场景:当前命名空间中成员在某个文件中应用的比较多,缺陷:冲突率较高
-
c++输入&输出
c++的输入是cin >> 内容;
比如cin >> a; 就是向 a 这个变量中输入值。
c++的输出是cout >> 内容 >> endl;
比如cout << a << endl; 就是输出 a 中的内容,endl 是换行的意思。
要注意的是输入输出需要调用标准库 std,以及包含头文件<iostream>- 可以直接使用using namespace std;
- 也可以使用using std::cin;…
- 或者直接在语句中这样写std::cin >> a;
这三种都是可以的。
-
缺省参数
缺省参数是声明或定义函数时为函数参数指定一个默认值,在调用该函数时,如果没有指定实参就采用这个默认值,否则使用制定的实参。
可以这样想,缺省参数就相当于备胎一样,对于女神来说,如果女神需要她的男神时(指定实参),备胎(缺省参数)就没有作用了,但是如果女神和他的男神吵架了,这时候备胎就该站出来了,哈哈。
比如:void test(int a = 0) { cout << a << endl; } int main() { test(); //没有传参,a 就是0 test(4); //传了参数,a 就是 4 }
缺省参数不能同时出现声明和定义中,否则会出现混淆,最好是放在声明的时候。
缺省参数分类:
- 全缺省参数(函数形参必须全部赋值):
void Test(int a = 1, int b = 2, int c = 3){ cout << a << " " << b << " " << c << endl; } int main() { //传参是从左向右传的 Test(10, 20 ,30); //输出10 20 30 Test(10, 20); //输出10 20 3 Test(10); //输出10 2 3 Test(); //输出缺省参数1 2 3 return 0; }
- 半缺省参数(部分参数带了缺省值,函数形参要从右往左依次赋值,不可以间隔着赋值)
void Test1(int a, int b, int c = 3){ cout << a << " " << b << " " << c << endl; } void Test2(int a, int b = 2, int c = 3){ cout << a << " " << b << " " << c << endl; } int main() { //有几个参数没有缺省值,就必须最少传几个缺省值 Test(10, 20 ,30); Test(10, 20); Test(10); //Test1不可以,2可以 Test(); //两个都不可以 return 0; }
还要注意缺省值必须是常量或者全局变量,在 c 语言中是不支持缺省参数的。
- 函数重载
函数重载是函数的一种特殊情况,c++ 允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表中的 形参个数 或 形参类型 或 不同形参的顺序必须不同,常用来处理实现功能类似数据类型不同的问题。
例如:int Add(int left, int right) { return left+right; } double Add(double left, double right) { return left+right; } long Add(long left, long right) { return left+right; } int main() { Add(10, 20); Add(10.0, 20.0); Add(10L, 20L); return 0; }
函数重载需要注意:它是在程序编译期间对函数的参数进行类型推演,根据推演的结果取选择合适的调用函数,如果没有最佳匹配函数,就尝试去转换,如果匹配不到,或者不明确(就是函数重载的几个函数都可以被调用),那么程序就会报错。
那么为什么c++能够支持函数重载呢?这就要了解一下名字修饰(name Mangling)
首先我们要知道在 c/c++中一个程序要运行起来需要经历预处理—>编译—>汇编—>链接。
而名字修饰是一种在编译过程中,将函数、变量的名称重新改编的机制,简单来说就是编译器为了区分各个函数,将函数通过某种算法,重新修饰为一个全局唯一的名称。换句话说,就是在底层给它重新命名以供编译器识别。
在c语言中,他的名字修饰规则非常简单,只是在函数名前面加了下划线。
我们常见到的error LNK2019: 无法解析的外部符号 _Add,该符号在函数 _main 中被引用。这里面就是给函数Add进行了名字修饰,只是简单的加了下划线。
在c++中,我们在vs中运行下面这段代码:
int Add(int left, int right);
double Add(double left, double right);
int main()
{
Add(1, 2);
Add(1.0, 2.0);
return 0;
}
编译链接后,编译器就会报下面的错误:
error LNK2019: 无法解析的外部符号 “double cdecl Add(double,double)” (?Add@@YANNN@Z)
error LNK2019: 无法解析的外部符号 “int __cdecl Add(int,int)” (?Add@@YAHHH@Z)
由此可以看出,c++的名字修饰规则复杂了许多。
double Add(double left, double right);
double cdecl Add(double,double)" (?Add@@YANNN@Z)
这两者可以看出,名字修饰后有函数的类型,以及参数类型,这里的cdecl是函数调用类型,在名字修饰过的名字中就是第一个A表示的意思;N表示 double 类型,H表示 int 类型,最后以“@”结束,以“Z”结尾。
面试题:
- C语言中为什么不能支持函数重载?
答:因为C语言底层使用的名字修饰规则太简单,只是在函数名前面加上一个下划线 _ ,而C++支持函数重载是因为C++底层会将函数的类型加入名字的修饰中。
例如:double Add (int left, double right);在C语言底层中,这个函数的名字就是 _Add而在C++底层中,这个函数的名字是 ?Add@@YANHN@Z - C++中函数重载底层是怎么处理的?
答:编译器会在底层的名字上加上类型修饰,才能实现函数重载。 - C++中能否将一个函数按照 C 的风格来编译?
答:在函数定义前加上extern "C"即可,这就是将代码按照C语言规则来编译。
extern “C”
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将 该函数按照C语言规则来编译。
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
-
引用
在 C 语言中函数传参有传值和传址两种方式。
C++又有一种新的形式—引用,引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
定义引用变量的格式:
类型& 名字 = 实体;
int a = 10;
int& ra = a;
ra = 20; //将 ra 的值改变,a 的值也会改变,因为他们是共用同一块内存空间。
要注意:引用变量的类型必须与其实体的类型一致,否则会报错。
引用的特性:
- 引用变量必须要初始化,不能这样写 int& ra; 需要给 ra 初始化。
- 一个变量可以有多个引用。
- 引用一旦引用一个实体,就再不能引用其他实体。
常引用:
const void TestConstRef() { const int a = 10; //int& ra = a; // 该语句编译时会出错,a为常量 const int& ra = a; // int& b = 10; // 该语句编译时会出错,b为常量 const int& b = 10; double d = 12.34; //int& rd = d; // 该语句编译时会出错,类型不同 const int& rd = d; }
常引用有三种写法:
1)const int& ra = 10;
2)const int a = 10;
const int& ra = a; //这种最常用
3) double d = 12.34;
const int& rd = d; //此处的 rd 不是对 d 的引用,可以看 rd 和 d 的地址是不同的
//那么对第三个解释一下:此处本来类型不同,是违背了引用的定义的,但是加上 const 为甚么会可以?是因为 double d = 12.34; 这条语句,会创建一个整型临时变量,然后const int& rd是对这个临时变量的引用。这个临时变量是不知道名字也不知道地址的。所以在后期修改 d 的值是不会改变 rd 的值。引用的应用场景:
- 直接给一个变量取别名
- 做函数的参数
- 做返回值
做返回值时,如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。因为如果函数返回时,离开函数作用域后,其栈上的空间已经还给系统,因此不能用栈上的空间作为引用类型返回。
看下面这段代码:
int& Add(int& left, int& right)
{
int ret = left + right;
return ret;
}
int main()
{
int a = 1;
int b = 2;
int& ret1 = Add(a, b);
int c = 3;
int d = 4;
int ret2 = Add(c, d);//执行到这句,ret1的值也会改变成ret2的值,原因见下面
printf("%d \n", ret2);
system("pause");
return 0;
}
综上,所以注意:如果按照引用的方式作为函数的返回值,不要返回栈上的空间如果要用就要让返回值不受函数影响—返回实体的生命周期比函数的声明周期长(例:用堆上函数的空间、全局变量、静态变量、引用类型变量)
效率:引用 约等于 传址 > 传值
- 指针和引用的区别?
答:看这段代码:
int main()
{
int a = 10;
int *pa = &a;
*pa = 20;
int& ra = a;
ra = 20;
system("pause");
return 0;
}
汇编代码及执行过程如下:
因此,两者在底层处理方式:一模一样
在底层:编译器在底层将引用按照指针的方式来进行处理的
引用实际就是指针 T& 就相当于 T* const
const T& 相当于 const T* const
在底层,引用变量实际是有空间的
概念层面:引用就是变量的别名,与其实体公用同一块内存空间,编译器不会为引用变量开辟新的内存空间。
区别:
1)引用在定义期间必须初始化,而指针没有要求
2)引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
3)没有NULL引用(就是引用必须初始化),但有NULL指针
4)在sizeof中含义不同;引用结果为引用类型的大小,但指针始终是地址空间所占字节数(32位平台下占 4 个字节,某些64位平台 8 个字节,有些64位系统是按32位系统来编译的https://blog.csdn.net/u010452239/article/details/70238105)
5)引用自加即引用的实体加 1 ,指针自加即指针向后偏移一个类型的大小
6)有多级指针,但是没有多级引用(有char **p,没有char&& rc = ch; 但是const char&& rrc = ‘c’;这种可以,叫右值引用,是c++11里的
7)访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8)引用比指针使用起来相对更安全(不会空引用,不能有空指针)
7. 内联函数
这里先了解下宏,宏有两种:宏常量 和 宏函数
在 C 语言中,宏定义是在预处理阶段被替换的,假如在宏定义时不小心将整型定义为字符型,那么编译后会报错,而且错误不好排查。
在 C++ 中,可以使用const关键字来定义常量,代替宏的作用。const 实在编译阶段被替换的,一旦出错,可以较方便的排查错误。
看下面这段代码:
int main()
{
const int a = 10;
int *pa = 100;
*pa = 100;
cout << a << endl;
cout << *pa << endl;
return 0;
}
在调试窗口可以看到,当改变*pa的值后,a 和 *pa 的值都发生改变,但是打印出来,a 的值却依然是 10.
这就是因为const定义的常量在编译阶段被替换,所以在打印的时候,a 的值就又被替换为10.
宏函数:
优点:提高代码运行的效率—在预处理阶段:用宏函数体替换调用位置,没有函数调用的开销因而提高了代码的运行效率。
缺点:1、因为是在预处理阶段替换,所以不会进行参数类型检测;
2、代码的复用性不好,容易造成代码膨胀(宏函数展开很多份,源文件越来越大);
3、不能调试;
4、造成副作用
内联函数处理:
在编译器编译阶段,用函数体替换函数调用的位置,少了函数调用压栈以及 战帧创建的时间开销,提高了代码的运行效率特性:
1. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使 用作为内联函数。
2.inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等 等,编译器优化时会忽略掉内联。
2. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
- auto关键字
在c++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指 示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
注意在使用auto关键字定义变量时要对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类 型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变 量实际的类型。
auto的使用规则:
1) auto声明指针类型时,用auto 和 auto* 没有任何区别,但用auto声明引用类型时必须加&
2) 在同一行定义多个变量时,这些变量必须是相同的类型(要是整型就都整型,浮点型则全浮点型),否则编译器会报错,因为编译器会根据第一个类型推导后面的其他变量。
-
基于范围的for循环
在c++11中,可以使用基于范围的for循环,for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二 部分则表示被迭代的范围。
比如:int a[] = {1, 2, 3, 4, 5};
for(auto& e : a) //遍历数组每个元素
e *= 2; //给数组中每个元素乘以2 -
指针空值nullptr
在c++98中,NULL不是空指针了,而是宏,在传统的C头文件(stddef.h)中,可以看到下面这段代码:#ifndef NULL #ifdef __cplusplus //判断代码是不是c++ #define NULL 0 //如果是c++,就将NULL 宏定义为0,这是int类型 #else #define NULL ((void *)0) //否则被定义为无类型指针(void*)的常量 #endif #endif
所以如果初始化指针变量 = NULL是错误的。
在c++11中,为了考虑兼容性,C++11并没有消除常量0的二义性,C++11给出了全新的nullptr表示空值指针。C++11为什么 不在NULL的基础上进行扩展,这是因为NULL以前就是一个宏,而且不同的编译器厂商对于NULL的实现可能 不太相同,而且直接扩展NULL,可能会影响以前旧的程序。
因此:为了避免混淆,C++11提供了nullptr, 即:nullptr代表一个指针空值常量。nullptr是有类型的,其类型为nullptr_t,仅仅可以被隐式转化为指针类 型,nullptr_t被定义在头文件中。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议好使用nullptr。