文章目录
前言
此文章是对C++入门上篇(上篇还包括小编先前创造的两篇C++文章,由于那部分知识比较晦涩,所以单独创作 )的一些零碎知识的总结,那么话不多说,请看下文。
一、C++关键字
C++关键字是C++编译器保留的特定单词,用于表示程序中的特定操作或属性,C++中共有 73 个关键字(C++ 11),包括了C语言中的 32 个关键字(红色标注)。
二、C++命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace 关键字的出现就是针对这种问题的。
2.1 命名空间的定义
定义命名空间,需要使用到 namespace 关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
//1. 普通的命名空间
namespace N1 // N1为命名空间的名称
{
// 命名空间中的内容,既可以定义变量,也可以定义函数
int a;
int Add(int left, int right)
{
return left + right;
}
}
//2. 命名空间可以嵌套
namespace N2
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N3
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
2.2 命名空间的使用
命名空间中成员该如何使用呢?比如我要访问下面代码中的 a ,该怎么访问呢:
namespace N
{
int a = 10;
int b = 20;
int Add(int left, int right)
{
return left + right;
}
int Sub(int left, int right)
{
return left - right;
}
}
int main()
{
//printf("%d\n",a); //该语句编译出错,无法识别a。
return 0;
}
那怎么访问 a 呢?
命名空间的使用方式有三种:
1、加命名空间名称及作用域限定符::
int main()
{
printf("%d\n",N::a);
return 0;
}
2、使用 using 将命名空间中成员引入
using N::b; //这里 b 就被展开了
int main()
{
printf("%d\n",b);
printf("%d\n",N::a);
return 0;
}
3、使用 using namespace 命名空间名称引入
using namespace N; //这里相当于把 N 全部展开
int main()
{
printf("%d\n",b);
printf("%d\n",a);
Add(10,20);
return 0;
}
2.3 C++ 中的 using namespace std;
C++中为了防止命名冲突,把C++库中的所有东西都定义在了一个 std 命名空间里面,也就是说你要使用 C++ 标准库里面的东西,你就要加 std , 同样如上,有三种方式进行使用:
1、指定命名空间;这是最规范的方式,但是缺点是很麻烦,每个地方都要指定。例如下面代码
#include<iostream>
//using namespace std;//如果加了这句,下面均不会编译错误
int main()
{
//cout << "hello world" << endl; //编译发生错误
std::cout << "hello world" << std::endl;
return 0;
}
2、全展开
如: using namespace std;
这种把 std 全部展开,相当于把C++标准库中的所有东西全部展开到全局区域里面去了,优点是使用起来很方便;缺点是如果自己定义了与库里面同名的东西,就会与 std 命名空间的定义产生冲突;所以在规范工程中不推荐使用这种方式,但是在日常练习中可以使用。
3、部分展开,即对库里面部分常用的一些对象或类型进行展开,而那些不常用的就用指定命名空间;这也是规范工程中常用的方式;例如下面代码
#include<iostream>
using std:: cout;
using std:: endl;
int main()
{
int a;
double b;
std::cin >> a >> b; //指定命名空间
cout << a << b << endl; //部分展开直接用
cout << "hello world" << endl;
return 0;
}
三、C++输入&输出
1、使用 cout 标准输出(控制台)和 cin 标准输入(键盘)时,必须包含 < iostream > 头文件以及 std 标准命名空间。( istream 输入流,ostream 输出流,iostream 标准输入输出流)
注意:早期标准库将所有功能在全局域中实现,声明在 .h 后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在 std 命名空间下,为了和 C 头文件区分,也为了正确使用命名空间,规定 C++ 头文件不带 .h ;旧编译器 (vc 6.0) 中还支持 <iostream.h> 格式,后续编译器已不支持,因此推荐使用 < iostream > + std 的方式。
2、使用C++输入输出更方便,不需增加数据格式控制,比如:整形-- %d,字符-- %c
3、cout 和 cin 对比 c语言中 printf 和 scanf 来说是可以自动识别类型的(函数重载+运算符重载实现)
#include<iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
//scanf("%d%lf%c",&a,&b,&c); //c语言的用法
cin >> a >> b >> c;
//printf("%d %f %c\n",a,b,c);
cout << a <<" "<< b <<" "<< c << endl;
return 0;
}
四、C++缺省参数
4.1 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
代码参考如下:
void TestFunc(int a = 0)
{
cout << a << endl;
}
int main()
{
TestFunc(); //没有传参时,使用函数的默认值0.
TestFunc(10); //传参时,使用指定的实参10
return 0;
}
4.2 缺省参数的分类
4.2.1 全缺省参数
代码参考如下
void testFunc1(int a = 10,int b = 20,int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
testFunc1();
testFunc1(1);
testFunc1(1,2)
testFunc1(1,2,3);
//不可以这样:testFunc1( , ,3);
//可以一个参数都不传,也可以传1/2/3个
return 0;
}
4.2.2 半缺省参数(缺省部分参数)
代码参考如下
void testFunc2(int a ,int b = 20,int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
}
int main()
{
//testFunc2(); //不可以不传
testFunc2(1);
testFunc2(1,2);
testFunc2(1,2,3);
return 0;
}
注意:
1、半缺省参数必须从右往左依次来给出,不能间隔着给。
例如:不能 void func ( int a = 10, int b, int c ) ( a给了缺省,那 b,c 也必须缺省; b 给了缺省,那 c 必须缺省)
2、 缺省参数不能在函数声明和定义中同时出现
//a.h
void testFunc3(int a = 10);
//a.cpp
void testFunc3(int a = 20);
{}
//如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用哪个缺省值。
3、传参也必须从左往右依次传参,不能空缺。
4、缺省值必须是常量或者全局变量。
5、C语言不支持缺省参数(编译器不支持)。
五、C++函数重载
5.1 函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
#include<iostream>
using namespace std;
//1.类型不同
int Add(int left,int right)
{
return left + right;
}
double Add(double left,double right)
{
return left + right;
}
long long Add(long left,long right)
{
return left + right
}
//int main()
//{
// Add(10,20); //调用 int Add(int left,int right)
// Add(10.0,20.0); //调用 double Add(double left,double right)
// Add(10L,20L);//调用 long long Add(long left,long right)
//2.个数不同
void Func1(int a,int b)
{}
void Func1(int a)
{}
void Func1()
{}
//3.顺序不同
void Func2(int a,char ch)
{}
void Func2(char ch,int a)
{}
int main()
{
return 0;
}
注意:返回值类型不同不能成为函数重载的条件,例如下面这个两个函数就不构成函数重载。(只是返回值不同不能构成重载)
double Add(double left,double right)
{
return left + right;
}
int Add(double left,double right)
{
return left + right;
}
5.2 名字修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持函数重载呢?
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理(头文件的展开、宏替换、条件编译、去掉注释)、编译(检查语法,生成汇编代码)、汇编(把汇编代码转成二进制的机器码)、链接(将目标文件链接在一起)。
这里的过程是什么呢?
1.组成一个程序的每个源文件通过编译之后都会分别转换成目标文件(目标代码)
2.每个目标文件由链接器联系在一起,形成一个单一完整的可执行程序。
3.链接的时候,链接器会到其他目标文件的符号表中为函数声明部分的地址空缺找到合适的地址进行匹配,将其需要的函数也链接到程序中。(链接阶段就是专门处理这种问题,链接器看到 test.o 调用 list_push_back,但是没有 list_push_back 的地址,就会到list.o的符号表中找 list_push_back 的地址,然后链接到一起,看下面的 第二张图)
编译和链接也有好几个步骤,更详细的如下
那么讲了这么久,函数重载和这里的关系是什么呢?
那就是,在C语言中,链接的时候去其他目标文件符号表找函数的时候,C语言就是用下图这里这样的一个名称,而C++ 不是用的这样的一个名称。C++用的名称加上了一个东西叫函数名修饰规则。
那么下面我们来看两张通俗易懂的图来了解 C++ 的函数名修饰规则是如何支持函数重载的,由于 Windows 下 vs 的修饰规则过于复杂,而 Linux 下 gcc(C语言)和 g++(C++) 的修饰规则简单易懂,下面我们使用了 gcc 和 g++ 演示了这个修饰后的名字:
采用C语言 (gcc) 编译器编译后结果:
结论:在linux下,采用 gcc 编译完成后,函数名字的修饰没有发生改变
采用C++ (g++) 编译器编译后结果
结论:在 linux下,采用 g++ 编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
通过上面我们可以看出 gcc 的函数修饰后名字不变。而 g++ 的函数修饰后变【_Z+函数长度+函数名+类型首字母】。所以当函数重载的时候,gcc下的两个函数的函数名称相同,编译阶段去找的时候就无法识别,而 g++ 下的重载函数因类型不同,个数不同,顺序不同都会导致编译下函数的名称不同,编译阶段去找的时候就能有效识别区分。
通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
另外我们也理解了,为什么函数重载要求参数不同!而跟返回值没有关系。
拓展知识(下面内容感兴趣的同学可深入学习,不感兴趣也可直接跳过)
Windows 下名字修饰规则
对比Linux会发现,windows下C++编译器对函数名字修饰非常诡异,但道理都是一样的。
【扩展学习:C/C++函数调用约定和名字修饰规则】
C++的函数重载
C/C++ 函数调用约定
5.3 extern “C” 的作用
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc 是 google 用C++ 实现的一个项目,他提供 tcmallc() 和 tcfree 两个接口来使用,但如果是C项目就没办法使用,那么他就使用 extern “C” 来解决。看下图理解:
作用:当 C 程序和 C++ 的程序 都想去调用一个 C++ 实现的一个静态库或者动态库,那 C++ 程序可以调动,而 C 语言程序无法调用,那我两个都想调用怎么办呢?在 C 语言程序的头文件里面包 C++ 库的这个 .h , 然后把这个地方的函数的一个声明之前加一个 extern “C” 就可以解决这个问题了。
注意:加了extern ”C“ 之后函数就不能重载了。
代码参考如下:
extern "C" int Add(int left, int right);
int main()
{
Add(1,2);
return 0;
}
有了上面的知识以后,我们也可以看出下面两个函数也不支持重载。
void func(int a)
{}
void func(int a = 10)
{}
int main()
{
func(); //两个都可以调,不知道调哪个
return 0;
}
注意:缺省参数符合函数重载的定义,但调用的时候编译器不识别函数重载调用哪个函数。
六、auto 关键字(C++ 11)
6.1 auto 简介
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有
人去使用它,为什么呢?
C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型
指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。(自动推导变量类型)
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
运行结果:
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为
变量实际的类型。
6.2 auto的使用规则
1、auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x; //int*
auto* b = &x; //int*
auto& c = x; //int //用auto声明引用类型时必须加&
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30
c = 40;
return 0;
}
2、在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对
第一个类型进行推导,然后用推导出来的类型定义其他变量
void Testauto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; //该行代码编译会失败,因为 c 和 d 的初始化表达式类型不同
}
6.3 auto 不能推导的场景
1、auto 不能作为函数的参数
// 此处代码编译失败,auto 不能作为形参类型,因为编译器无法对 a 的实际类型进行推导
void Testauto(auto a)
{}
2、auto 不能直接用来声明数组
void Testauto()
{
int a[] = {1,2,3};
// auto b[] = {4,5,6}; //该行会编译出错
}
3、为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4、auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等
进行配合使用。
七、基于范围的for循环(C++11)
7.1 范围 for 的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
cout << *p << endl;
}
而对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中
引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,
第二部分则表示被迭代的范围。
void TestFor()
{
//自动遍历,依次取出array 中的元素 ,赋值给e, 直到结束
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
//C++ 11新语法遍历 特点:写起来比较简洁
}
注意:与普通循环类似,可以用 continue 来结束本次循环,也可以用 break 来跳出整个循环。
7.2 范围 for 的使用条件
1、for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供 begin 和 end 的
方法,begin 和 end 就是 for 循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void Testfor(int array[])
{
for(auto& e: array) //因为array已经不再是数组了,而是已经退化成指针了(数组传参以后就退化成指针)
cout << e << endl;
}
2、迭代的对象要实现 ++ 和 == 的操作。
八、指针空值nullptr(C++11)
8.1 C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ......
}
NULL实际是一个宏,在传统的C头文件 (stddef.h) 中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数,但是由于 NULL 被定义成0,因此与程序的初衷相悖。
在 C++98 中,字面常量0既可以是一个整形数字,也可以是无类型的指针 (void*) 常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意:
1、在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新****关键字引入的。
2、在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3、为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
结尾撒花
本章到此已经全部结束,本章节我们讲述了大量的关于C++基础的零散的知识,相信看到这里的你们也收获很多,关于以上内容有错误或者欠缺的也欢迎大家踊跃指出,小编以后定当给大家贡献更精彩的文章 ,那最后,喜欢小编的文章的话,记得点赞评论收藏+关注哦,拜拜,我们下篇文章见!