命名空间
在c++中,变量、函数和大量的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对这些标识符的名称进行本地化,以避免命名冲突或名字污染,namesapce关键字的出现就是针对这种问题的。
命名空间的定义
定义命名空间需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
一、正常的命名空间定义
//一般项目中是用项目名做命名空间名
namespace project1
{
//命名空间中可以定义变量,函数等
int x;
int Sub(int left,int right)
{
return left-right;
}
}
二、命名空间可以嵌套
namespace project1
{
int x;
int y;
int Add(int left,right)
{
return left+right;
}
namespace project1
{
int a;
int b;
int Sub(int left,right)
{
return left-right;
}
}
}
三、同一个工程中允许出现多个相同的命名空间,编译器最后会合成同一个命名空间中
P.S 一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
命名空间的使用
命名空间的使用有三种方式:
一、加命名空间名称及作用域限定符
符号“::”在C++中叫做域作用限定符,我们通过“命名空间名称::命名空间成员”的方式便可以访问到命名空间中相应的成员。
int main()
{
printf("%d\n",N::a); //::为域作用限定符
return 0;
}
二、使用using将命名空间中的某个成员引入
我们可以通过“using 命名空间名称::命名空间成员”的方式将命名空间中指定的成员引入。这样一来,在该语句之后的代码中就可以直接使用引入的成员变量了。
using N::b
int main()
{
printf("%d\n",N::a);
printf("%d\n",b);
return 0;
}
三、使用using namespace将命名空间名称引入
第三种方式就是通过”using namespace 命名空间名称“的方式将命名空间中的全部成员引入。这样一来,在该语句之后的代码中就可以直接使用该命名空间内的全部成员了。
using namespace N;
int main()
{
//其中a和b是命名空间N中的成员
printf("%d\n",a);
printf("%d\n",b);
return 0;
}
在项目中我们通常是把常用的展开,例如像“using std::cout”这样指定展开,因为像“using namespce std”这样全部展开会有风险,我们定义如果和库重名就会报错,不过像日常练习我们可以那样做,项目中建议指定访问。
C++输入与输出
下面我们来看一段代码
#include<iostream>
//std是c++标准库的命名空间的名称,c++将标准库的定义实现都放在这个命名空间中
using namespace std;
int main()
{
cout<<"Hello world"<<endl;
return 0;
}
我们知道c语言中用标准输入输出函数“scanf”,"printf"实现输入与输出,而在c++中我们使用cout标准输出对象(控制台)和cin标准输入对象(键盘)代表输入与输出,其中在使用“cin”和“cout”时必须包含<iostream>头文件以及按命名空间使用方法使用std。
说明:
1.cout和cin是全局的流对象,endl是特殊的c++符号,表示换行输出,他们都包含在包含<iostream>头文件中。
2.<<是流插入运算符,>>是流提取运算符 。
3.c++的输入输出可以自动识别变量类型(代码如下)
#include<stdio.h>
#include<iostream>
using namespace std;
int main()
{
int x = 10;
double d = 11.11;
// 自动识别类型
cout << x << " " << d << endl;
// >> 流提取运算符
cin >> x >> d;
cout << x << " " << d << endl;
printf("%d,%.2f\n", x, d);
return 0;
}
缺省参数
一、缺省参数的概念
缺省参数是声明或定义函数时为函数参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Fun(int a = 0)
{
cout<<a<<endl;
}
int main()
{
Func(); //没有传参时,使用参数的默认值
Func(10); //传参时,使用指定的实参
}
二、缺省参数分类
缺省参数分为全缺省参数和半缺省参数
//半缺省型
void Func2(int a, int b = 6, int c = 8)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
//全缺省型
void Func3(int a = 6, int b = 8, int c = 10)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
int main()
{
//Func2(6,6,8);
Func2(6);
//Func3(6,8,10);
Func3();
return 0;
}
注意:1、半缺省参数必须从右往左依次给出,不能间隔着给
2、缺省参数不能在函数声明和定义中同时出现
函数重载
一、函数重载的概念
函数重载是函数的一种特殊情况,c++允许在同一个作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现几个功能类似数据类型不同的问题。
1.参数类型不同
int Add(int x, int y)
{
cout << "int Add(int x, int y)" << endl;
return x + y;
}
double Add(double x, double y)
{
cout << "double Add(int x,int y)" << endl;
return x + y;
}
int main()
{
cout << Add(10, 20) << endl;
cout << Add(10.1, 20.2) << endl;
return 0;
}
2.参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a = 0)
{
cout << "f(int a)" << endl;
}
int main()
{
f(3);
return 0;
}
3.参数类型顺序不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char a, int b)
{
cout << "f(int b,char a)" << endl;
}
int main()
{
f(1, 'x');
f('x', 1);
return 0;
}
另外,我们要注意一种特殊情况
void f()
{
cout << "f()" << endl;
}
void f(int a = 1)
{
cout << "f(int a)" << endl;
}
int main()
{
f();
return 0;
}
上述代码会报错,原因是无参调用存在歧义,"f"对重载函数的调用不明确。
二、函数重载的原理
为什么C++支持函数重载,而C语言不支持函数重载呢?
我们知道,一个C/C++程序要运行起来都需要经历以下几个阶段:预处理、编译、汇编、链接。
我们知道,在编译阶段会将程序中的每个源文件的全局范围的变量符号分别进行汇总。在汇编阶段会给每个源文件汇总出来的符号分配一个地址(若符号只是一个声明,则给其分配一个无意义的地址),然后分别生成一个符号表。最后在链接期间会将每个源文件的符号表进行合并,若不同源文件的符号表中出现了相同的符号,则取合法的地址为合并后的地址(重定位)。
在C语言中,汇编阶段进行符号汇总时,一个函数汇总后的符号就是其函数名,所以当汇总时发现多个相同的函数符号时,编译器便会报错。而C++在进行符号汇总时,对函数的名字修饰做了改动,函数汇总出的符号不再单单是函数的函数名,而是通过其参数的类型和个数以及顺序等信息汇总出 一个符号,这样一来,就算是函数名相同的函数,只要其参数的类型或参数的个数或参数的顺序不同,那么汇总出来的符号也就不同了。
引用
一、引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。 使用的形式为“类型& 引用变量名(对象名)=引用实体”
二、引用的特性
1.引用在定义时必须初始化
void Test
{
int a=10;
int& ra=a; //定义引用类型
printf("%d\n",a);
printf("%d\n",ra);
}
2.一个变量可以有多个引用
void test()
{
int a=10;
int& b=a;
int& c=a;
printf("%d %d %d",a,b,c);
}
3.引用一旦引用一个实体,再不能引用其他实体
void Test
{
int a=0;
int& b=a;
int& c=b;
int x=1;
int c=x;
}
上述代码,c引用的实体是b,b引用的实体是a,即b是a的别名,c是b的别名,那么c也是a的别名,c和b也就不能引用其他实体。那么如果我们想要改变b,c的值,只需要直接赋值即可,需要注意的是此时将x的值赋给c,相当于a也被赋值了。
三、使用场景
1.做参数
形参是实参的别名,形参改变,实参就改变,使用引用做参数能大大提高传参效率
void Swap1(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
2.做返回值
引用做返回值不仅可以减少拷贝,提升效率,还能修改返回值和获取返回值
int& Count()
{
static int n=0;
n++;
//返回n的别名
return n;
}
我们需要注意的是上述代码如果int前没有static修饰,那么n就是局部变量,当函数调用结束后,函数栈帧就会销毁,栈空间使用权返还给操作系统(但空间本身还在),空间一旦进行了栈帧清理可能会发生错误。
例如
int& Count()
{
int n=0;
n++;
return n;
}
int main()
{
//ret是n的别名的别名,即是n的别名
int& ret=Count();
cout<<ret<<endl;
return 0;
}
这里打印的ret的只是不确定的,如果Count函数结束,栈帧销毁,没有清理栈帧,ret结果侥幸正确,如果Count函数结束,栈帧销毁,清理了栈帧,那么ret结果是随机值。在main中ret引用Count函数返回值,实际引用的就是一块已经被释放的空间。
下面再来几个例子
double d=1.11;
int i=d; //不同类型间转化产生临时变量
const int& ri=d; //可以
int& ri=d; //不可以,会报错(权限进行了放大)
当ri引用d时(ri是d的别名),我们由“不同类型间转化会产生临时变量”知,由于d是double类型,d会生成临时变量,而临时变量具有常属性(只能读不能写),我们知道引用过程中引用权限不能放大,因此"int& ri=d"这条语句会报错。
cosnt int c=0;
int d=c; //可以
这个可行是因为c是拷贝赋值给d,不涉及引用权限放大的问题,d的改变不影响c。
int x=0;
int& y=x; //可行,权限平移
const int& z=x; //可行,权限缩小
++x; //x自身可以修改,z本身不可以修改,x改变了z也发生改变
引用权限可以平移和缩小,但不能放大。
总结:1.基本任何场景都可以使用引用传参。 2.谨慎使用引用返回。如果出了函数作用域,返回对象不在了(返还给操作系统),就不 能用引用返回,还在就可以使用。用一句话概括就是:局部对象(变量)不能使用引用 返回。
四、指针和引用的区别
1.引用在概念上定义一个变量的别名,指针存储一个变量的地址。 2.引用在定义时必须初始化,指针没有要求。 3.引用在初始化时引用一个实体时就不能再引用其他实体,而指针可以在任何时候指向一个同类型的实体。 4.没有NULL引用,但有NULL指针。 5.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。 6.引用自加即引用的实体加一,指针自加即指针向后偏移一个类型的大小。 7.有多级指针,但是没有多级引用。 8.访问实体方式不同,指针需要显示解引用,引用编译器自己处理。 9.引用比指针使用起来相对更安全。
内联函数
一、内联函数的概念
以inline修饰的函数叫内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
二、内联函数的特性
1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。 2.inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长),不是递归、频繁调用的函数用inline修饰,否则编译器会忽略inline特性。 3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
auto关键字(C++11)
一、auto的介绍
早期c/c++中auto含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但一直没有什么人去使用它。在c++11中标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译期间推导而得。
int main()
{
int a = 0;
int b = a;
auto c = a; // 根据右边的表达式自动推导c的类型
auto d = 1 + 1.11; // 根据右边的表达式自动推导d的类型
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
}
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种类型的声明,而是一个类型声明的占位符,编译器在编译期会将auto替换为变量实际的类型。
二、auto的使用细则
1.auto和指针结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&
int main()
{
int x=10;
auto a=&x;
auto* b=&x;
auto& c=x;
cout<<typeid(a).name()<<endl;
cout<<typeid(b).name()<<endl;
cout<<typeid(c).name()<<endl;
return 0;
}
2.同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同类型,否则编译器会报错,因为编译器实际值对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{
auto a=1,b=2;
auto c=3,d=4.0; //该行代码会编译失败,因为c和d的初始化表达式类型不同
}
3.auto不能作为函数的参数
//此处代码会编译失败
void Test(auto a)
{
}
4.auto不能直接用来声明数组
void Test()
{
int a[]={1,2,3};
auto b[]={4,5,6}; //报错
}
基于范围的for循环(C++11)
一、范围for的语法
在c++11中引入了基于范围的for循环。for循环后的括号由冒号“:”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void Test()
{
int arr[]={1,2,3,4,5,6};
for(auto& e : arr)
e*=2;
for(auto e : arr)
cout << e << " ";
return 0;
}
二、范围for使用的条件
1.范围for迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素与最后一个元素的范围;对于类而言,应该提供beign和end的方法,begin和end就是for循环的迭代范围。
2.迭代对象要实现++和==操作。后面我们会介绍
指针空值nullptr(C++11)
NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在
使用空值的指针时,都不可避免的会遇到一些麻烦。因此C++11中引入了nullptr表示指针空值。
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。