c++ 基础概念
Posted 正义的伙伴啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++ 基础概念相关的知识,希望对你有一定的参考价值。
C++基础概念
文章目录
1.C++关键字
由于c++是向下兼容c语言的,所以c语言的32个关键字c++都会包含,此外c++新增了一些其他的关键字,使c++的关键字增加到了63个
2.命名空间
1.为什么要命名空间:
我们在C/C++中,变量、函数名如果定义在全局作用域中,可能会给下面程序定义变量造成冲突。例如
#include<stdio.h>
int printf = 0; //与库函数 里面的 printf 函数名重合
int a = 0;
//int a = 0; //与上面的a都定义在全局作用域里面,这里就是重定义了
int main()
{
int a = 0; //与上面的a的定义不在同一个作用域,所以这里是正确的
return 0;
}
从上面的例子可以看出C语言在处理变量名上有一些问题,这时C++就引入了命名空间 对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题。
2.如何定义和使用命名空间
- 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员:
namespace N1 //命名空间的名称
{
int a;
int add(int left, int right) //还可以定义函数
{
return left + right;
}
}
- 此外命名空间还可以嵌套定义:
namespace N1 //命名空间的名称
{
int a;
int add(int left, int right) //还可以定义函数
{
return left + right;
}
namespace N2
{
int b;
}
}
//这里如果要调用N2中的b 要先调用N1再调用N2
- 同一个工程允许存在多个相同名称的命名空间,编译器在以后会合成同一个命名空间。
namespace N1 //命名空间的名称
{
int a=3;
int add(int left, int right) //还可以定义函数
{
return left + right;
}
namespace N2
{
int b;
}
}
namespace N1 //命名空间的名称
{
int a2;
int add2(int left, int right) //还可以定义函数
{
return left + right;
}
}
注意: 命名空间里的变量只能在其的作用域里使用
3. 如何使用命名空间
命名空间有三种使用方法:
- 加命名空间名称及作用域限定符
int mian()
{
N1::a = 3;
printf("%d", N1::a);
return 0;
}
这里::
限定符使得我们能使用命名空间N1里a名称的权限,并对其赋值和打印,虽然这里有效的避免了命名污染的情况,但是每次对命名空间里名字的使用都要写成这种形式会十分麻烦。
- 使用
using
将命名空间中常用成员引入
using N1::a;
int mian()
{
printf("%d", a);
return 0;
}
这种是最优的解决方法
- 使用
using namespace
命名空间名称引入
using namespace N1;
int mian()
{
printf("%d", a);
return 0;
}
这里是把N1里面所有的名称都展开了,但缺点很显而易见,就是可能会造成命名污染。
这里还要提到——std
C++标准库中的函数或者对象的名称都在std中定义,举个例子:所以我们以后写程序时要写到的cin 和 cout 函数都要写成std::cin
和std::cout
,但是我们可以通过using namespace std
来避免这种写法。注意要使用C++标准库里的函数必须要引头文件!
#include<iostream> //存放标准库里函数的定义
using namespace std; //可以对库里函数名称使用的权限
3. C++输入&输出
我们都知道C语言是不自带输入输出,要借助输入输出函数来完成。C++引入了新的输入和输出
#include<iostream>
using namespace std;
int mian()
{
cout << "hello word" << endl;
return 0;
}
注意
- C++输入输出函数要包含头文件
#include<iostream>
- C++输入输出自动识别数据类型,不需增加数据格式控制,这种固然方便了很多,但是有些情况却不能胜任,例如要输出一个浮点数小数点后2位,cout就无法完成。所以用
scanf / cin
或printf / cout
完全是看需求
4. 缺省参数
1.缺省参数的概念
缺省参数是声明或定义函数时为函数参数指定的一个默认值。调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。
#include<iostream>
using namespace std;
void fun(int a = 0)
{
cout << a << endl;
}
int mian()
{
fun(); //没有参数输入默认就是0
fun(10);
return 0;
}
2.缺省参数的分类
- 全缺省参数
字面意思即所有参数都有一个默认值
void fun(int a = 0, int c = 10l,int d=2)
{
cout << a << endl;
}
- 半缺省参数
void fun(int a, int c = 10l,int d=2)
{
cout << a << endl;
}
但是半缺省参数必须从右往左依次给出来,因为如果从左到右给出缺省参数,那么如果函数传值过来到底是给缺省参数赋值还是给非缺省参数赋值,我们就不得而知了。
3.注意事项
- 缺省参数不能在函数声明和定义中同时出现,因为如果 声明 和 定义 中缺省参数的值不一样,会造成编译器无法确定该用哪个缺省值
- 缺省值必须是 常量 或 全局变量
- C语言中不能使用缺省参数
5.函数重载
1.函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一个命名空间声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
int add(int x, int y)
{
return x + y;
}
double add(double x, double y)
{
return x + y;
}
long add(long x, long y)
{
return x + y;
}
以上就是add函数的三个重载函数
int add(int x, int y)
{
return x + y;
}
double add(int x, int y)
{
return x + y;
}
注意!!!! 函数的返回值类型不同是不能作为重载函数的,虽然编译器是可以分辨出返回值的类型不同,但是 函数在调用的过程中是无法确定返回值的类型的,所以编译器无法确定调用哪个重载函数的。
2. 重载函数的底层原理
——为什么C语言能不能实现函数重载,而C++却能实现呢?
首先我们要了解一下程序编译的过程:
这里要重点理解符号表的生成,符号表是一个文件所有 函数名+函数地址 所生成的一张表。而C++和C语言最大的区别就是在生成符号表的时候对函数名的处理有所不同
- C语言对于符号表的函数名一般就是直接使用函数名
- C++不仅使用函数名,还把参数列表的特征加入到了符号表的函数名中,这样具有不同参数列表的函数就具有了不同的特征,使得重载函数得以区分
3. extern “C”
这个语句是为了使某些C++的代码在C语言中也能使用,因为我们知道C++是向下兼容C语言的,C++中可以使用C语言,但是在C的代码中却不能使用C++的特性。例如:在一个C++程序中,可以调用用C语言使用的库,但是在C语言编写的程序中,却不能直接调用C++编写的库。
这时extern "C"语句会将某些函数按照C的风格来编译
extern "C" int add(int x, int y);
注意
函数的缺省参数是不能作为区分函数参数列表的特征的,所以也就不能成为重载函数,例如
int add(int x, int y)
{
return x + y;
}
int add(int x, int y=0)
{
return x + y;
}
6.引用
1. 引用也是C++新引入的一个概念:
引用不是新定义了一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量 共用同一块内存空间。
定义一个引用实体:
类型 & 引用变量名(对象名) =引用实体
int main()
{
int a = 10;
int& b = a; //这里的&标识符指的是 引用
printf("%p\\n", &a); // 下面的&运算符指的是取地址运算符
printf("%p\\n", &b);
}
这里就验证了引用变量和应用实体实际上共用的是同一块空间
引用实际上和C语言中的指针很像,实现的功能也大致相同,引用在使用的时候可以用作函数参数实现和指针传地址一样的效果,引用做参数有一下几个优点:
- 输出型参数
- 当函数参数比较大的时候,相比于传值,引用做参数可以较少拷贝
注意:
- 这里要和取地址符的含义区分清楚,
&
有且只有在定义的时候才用作引用,其他 的时候都是用作取地址符 - 引用类型必须和引用实体的类型相同
- 引用一旦引用一个实体,再不能引用其他实体
- 引用一定要初始化,
int& r;
这种写法是错误的 - 一个变量可以有多个引用
2. 常引用
常引用是对常数的直接的引用,这时就不能直接引用,而是要加上关键字const
来区分
int main()
{
int& a = 10; //错误的,常量必须要加const
const int& b = 10;
int& c = b; //错误的,常量必须要加const
const int& d = b;
int x = 20;
const int& y = x; //这里其实将权限进行缩小,原本是可以x是可以修改的,引用y可以缩小权限使其无法被修改,反过来就不行了
x = 30;
y = 40;//const限制了y的权限,所以引用y是不可以修改的
const int p = 20;
const int& ps = p; //这里因为p是const类型,他的引用就必须是const类型,p和ps都无法修改
}
这里还要注意一种特殊的常引用:
int main()
{
int a = 1;
double b = 2;
const int& c = b;
}
这里涉及到整型提升的过程,在double 向 int 类型转换时会生成一个临时变量,而临时变量也会被当成常量来对待,所以这里必须要加上const
常引用的应用:
- 我们在函数传参的时候如果不想参数在函数中被无意修改,可以使用常引用来避免这种情况
例如:要求一个数的立方
int cube1(int x)
{
x *= x * x;
return x;
}
int cube2(int& x)
{
x *= x * x;
return x;
}
int main()
{
int x1 = 3;
int x2 = 3;
cout << cube1(x1) << endl;
cout<<"x1:" << x1 << endl;
cout << cube2(x2) << endl;
cout << "x2:" << x2 << endl;
}
传引用造成了x2的值也被修改这是我们不想看到的,所以这里最好使用const int& x
来避免这种无意中产生的错误。
- 传入的参数类型正确,但是不是左值
int cube1(int& x)
{
return x * x * x;
}
int cube2(const int& x)
{
return x * x * x;
}
int main()
{
cout << cube1(x1 + 1) << endl;//这个是错误的,表达式属于右值必须用常引用接收
cout << cube2(x1 + 1) << endl;//传参为表达式
cout << cube2(1) << endl;//传参为字面常量
}
这里解释一下左值 和 右值:
左值:是可以被引用的对象,例如:变量、数组元素、结构成员
右值:字面常量、表达式
上述两种情况必须使用常引用来解决。
- 传入的参数类型不正确,但是能发生转换
int add(const int& x)
{
return x * x;
}
int main()
{
double x = 3.0;
cout << add(x) << endl;
}
这里发生了一个隐式类型转换从double -> int
,而转换的实质是生成一个临时变量,有这个临时变量在进行传参,而对临时变量的引用必须使用常引用。所以修改引用并不会影响本身值的改变,就相当于C语言的传值传参传过去的是本身的一个拷贝,但是有规定:常引用是const
类型无法修改,所以就不会出现这种问题了
引用做参数要尽可能使用const的原因
- 使用const可以避免无意中修改引用实参
- 使用const之后,就可以接收const 和 非const 值
- 使用const可以使函数生成正确的临时变量
3.引用的使用场景
- 做参数
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
和C语言中传地址实现的时同一个道理
- 做返回值
int& count()
{
static int n = 0;
n++;
return n;
}
做返回值还可以进行进一步探究:
int add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int ret = add(1, 2);
cout << ret << endl;
return 0;
}
这是一个非常简单的程序,我们对他进行一点改变:
int add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
//int& ret = add(1, 2); //错误写法
const int& ret = add(1, 2); //正确写法
cout << ret << endl;
return 0;
}
因为我们知道在调用函数的时候会创建函数栈帧,函数调用完的时候函数栈帧就会被销毁,返回值实际上是编译器生成的一个临时变量返回给ret的,由上面的知识可知临时变量要用常引用接收。
但如果我们把返回值的类型改成引用类型:
int& add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = add(1, 2);
add(3, 4);
cout << ret << endl;
return 0;
}
这里 函数的返回值是 一个引用,所以这里返回的是c的一个别名,所以这里ret实际上也就是c的一个别名,共用一个地址空间。
但是这里实际上是一个非法访问问题,ret是c的一个别名,但是函数结束后,函数空间就会被销毁,但是为什么c的值还被保留下来了?
这是vs编译器的特殊原因,vs编译器对待函数栈帧销毁并不会对其内存清空,所以c的值虽然还存在内存中,但是函数运行结束按理说就无法访问那片内存了,但是ret是c的别名对齐非法访问。
为什么最后ret的值为7?
了解了上面的原理之后,其实就很好理解了。add(3, 4);
在开辟函数栈帧的时候和add(1, 2);
开辟的函数栈帧是完全一样的,只是传入的值不一样,所以两个函数栈帧储存c的内存位置都是相同的,顾会修改c的值,c的别名ret也会跟着修改
函数栈帧图:
int& add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = add(1, 2);
printf("hello world");
cout << ret << endl;
return 0;
}
如果在输出ret前随便加一个和add不同的函数,就会破坏c内存空间储存的值
函数引用返回总结
- 出了函数的作用域,变量会被销毁的(临时变量),就不能用引用返回
- 出了函数的作用域,变量不会被销毁(静态变量),可以使用引用返回
4.引用 与 指针的区别
指针 | 引用 |
---|---|
指针在声明和定义可以分开 | 引用必须有初始值,不能只声明不定义 |
指针存在空指针 | 引用不存在空值 |
用sizeof计算时,地址空间始终是地址空间所占的字节数 | sizeof引用的时候计算的是引用类型的大小 |
有多级指针 | 但没有多级引用 |
指针自加即指针向后偏移一个类型的大小 | 引用自加即引用实体增加1 |
由此可以看出引用比指针更加安全,既避免了空指针的问题,还简化了多级指针解引用的问题。在C++中引用是对指针不错的一个替代。
7.内联函数
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
首先要了解一下函数函数在调用的时候的底层原理:
计算机在运行程序时,操作系统将这些指令载入内存中,因此每条指令都在内存中有特定的内存地址。在执行到函数调用指令的时候,程序将在函数调用后立即储存该指令的地址,并在堆栈上开辟新的空间,执行函数内部的代码指令,然后跳回到地址被保存的指令处(有个形象的比喻是:类似于看书看到一个注释,然后跳转到去看注释,看完之后跳转后继续阅读正文)
由此可以看出来回跳跃并记录位置会有一定程度上的开销,所以C++提出了内联函数。内联函数直接在调用的地方展开,就省去了储存函数调用指令。
注意:
- inline是一种以空间换时间的做法,省去调用函数额外开销。所以代码很长或者有循环/递归的函数不适宜作为内联函数
- inline对于编译器只是一个建议,至于编译器会不会对内联函数进行展开,编译器会自己做出判断。
- inline函数 声明 和 定义 放在一起,在定义和声明的函数名前面都加上关键字inline。不然程序会出现链接失败。
内联函数 与 宏 的区别
与内联函数相比 宏的缺点很明显:
- 宏没有类型检查,但是内联函数有类型检查,同时还可以发生类型转换,例如函数形参的类型是int,而传进来的参数是double,此时就会发生类型转换使double转换成int类型。
- 宏 不是基于按值传递,本质上还是一种替换的思想 例如:
#define fun(x) x*x*x
int main()
{
int a = 1;
cout << fun(++a) << endl;
return 0;
}
本来想传入的使++a的值也就是2,但实际上 表达式为:(++a)*(++a)*(++a)
结果使:64
所以内联函数在 宏的基础上改进了不少
8.auto关键字
auto是一种新的类型,用来让编译器推导变量的类型。推导的过程在编译过程
使用auto必须初始化,,在编译时期编译器会根据变量的类型推导出变脸的类型并将其替换。auto类似于一个待定符号,编译会自动将其替换成正确的类型
使用规则:
- 用auto声明指针类型时,用
auto p
或auto *p
没有任何区别,但用auto声明引用类型时则必须加&
int x = 10;
auto a = &x;
auto* b = &x;
auto& d = x;
- 同一行声明多个变量的时候,类型必须相同,否则编译器会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 10, b = 20;
auto a = 10, b = 'c';
注意:
- auto不能作为函数参数,因为编译器无法对形参进行类型推导
- auto不能直接用来声明数组
9.基于范围的for循环
C++中提出了一种新的基于for 的循环
形式
for(第一部分:用于迭代的变量 : 第二部分:被迭代的范围)
int main()
{
int arry[] = { 1,2,3,4,5,6 };
for (auto& e : arry) // 如果想改变数组中的变量必须使用&引用
{
e *= 2;
}
for (auto e : arry)
{
cout << e << " ";
}
}
与普通循环相似,也可以使用break
和 continue
来跳出循环。
10.指针空值
C++中对于NULL的定义其实是一个宏,而宏的值恰好为零。空指针实际上是内存按字节为单位空间的编号,空指针并不是不存在的指针而是内存第一个字节的编号。
由于这个对NULL的宏定义造成了一些问题:
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* x)
{
cout << "f(int *x)" << endl;
}
int main()
{
f(0);
f(NULL);
}
运行结果:
因为NULL的宏定义为0,所以这里编译器就认为NULL是int类型,如果非要让0当作指针使用必须写成(void *)0
。所以C++为了避免这种情况引入了nullptr
nullptr在使用的时候不用引头文件,在今后写代码的过程中尽量使用nullptr
以上是关于c++ 基础概念的主要内容,如果未能解决你的问题,请参考以下文章