文章目录
1.命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
- 正常的命名空间定义
#include<stdio.h>
int a = 1;
// 命名空间中可以定义变量/函数/类型
namespace czq
{
int a = 2;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}//注意:这里没有;
//展开命名空间
using namespace czq;
//局部域 -> 全局域 -> 展开命名空间域 -> 指定命名空间域
int main()
{
int a = 0;
printf("%d\n", a);//局部域
printf("%d\n", ::a);//全局域
printf("%d\n", czq::a);//命名空间域
return 0;
}
- 命名空间可以嵌套
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
int main()
{
printf("%d\n", N1::a);
printf("%d\n", N1::N2::c);
return 0;
}
注意:
- 在同一个域中命名不能相同,不同的域可以;
- 全局和局部冲突时局部优先;
- 编译器默认不会去命名空间域搜索,需要人为展开or指定才能搜索;
- 展开了命名空间域相当于暴露在全局域中,如果与全局域中出现相同命名,这时会形成访问冲突;
- 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。(相同命名空间发生命名冲突时可以嵌套解决。
2.C++的输入和输出
输入和输出的使用:
#include<iostream>
using namespace std;
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
int main()
{
int a = 0, b = 0;
// << -- 流插入运算符
// cout==printf
cout << "请输入a和b" << endl;//endl=='\n'
// >> -- 流提取运算符
// cin==scanf
cin >> a >> b;
cout << "a=" << a << endl << "b=" << b << endl;
//C++可以自动进行类型匹配
return 0;
}
说明:
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
- <<是流插入运算符,>>是流提取运算符。
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型。- cout默认保留一位小数,因为C++兼容C语言,所以建议使用C语言的方法进行控制精度与宽度。
- cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识。
注意: 早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支持<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。
std命名空间的使用惯例:
- std是C++标准库的命名空间,在日常编程练习时,由于代码量比较少,可以直接using namespace std 进行全部展开;
- 但在大型项目中,using namespace std 标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题;所以在大型项目中建议像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。
3.缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
缺省参数分为:全缺省和半缺省
using namespace std;
void FUNC(int a = 10, int b = 20, int c = 30)
{
cout << "a=" << a << endl;
cout << "b=" << b << endl;
cout << "c=" << c << endl;
}
int main()
{
//FUNC();//全缺省 -- 没有传参时,使用参数的默认值
FUNC(1,2);//半缺省 -- 传参时,使用指定的实参
return 0;
}
注意:
- 全缺省时,必须要给参数的默认值;半缺省时,传参的可以不给参数的默认值,没有传参的必须要给参数的默认值。
- 半缺省参数必须从右往左依次来给出,不能间隔着给。
- 缺省参数不能在函数声明和定义中同时出现,同时出现编译器无法确定使用哪个参数,所以当声明和定义分离时,缺省参数要给声明,不给定义,因为.h文件中包含的是声明。
- 缺省值必须是常量或者全局变量。
- C语言不支持缺省参数(编译器不支持)
4.函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
- 参数类型不同
#include<iostream>
using namespace std;
void Add(int left, int right)
{
cout << "int: left + right = " << left+right << endl;
}
void Add(double left, double right)
{
cout << "double: left + right = " << left + right << endl;
}
int main()
{
Add(10, 20);
Add(2.5, 7.5);
return 0;
}
- 参数个数不同
#include<iostream>
using namespace std;
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
f();
f(1);
return 0;
}
- 参数类型顺序不同
#include<iostream>
using namespace std;
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
f(1, 'a');
f('b', 2);
return 0;
}
补充说明:
- C语言进行编译后,是通过函数名进行匹配函数,同名函数无法区分,所以不支持重载;C++编译后,通过函数修饰规则来区分,只要参数不同,修饰出来的名字(在linux下:_Z+函数长度+函数名+类型首字母)就不一样,就支持了重载。
- 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
5.引用
引用:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
例如:
#include<iostream>
using namespace std;
int main()
{
int a = 0;
//一个变量可以有多个引用
int& b = a;//b是a的引用(别名)
int& c = a;//c是a的引用(别名)
int& d = c;//d是c的引用(别名)
b++;
//引用在定义时必须初始化
//int& rb;(X)
//引用一旦引用一个实体,再不能引用其他实体
//int x = 10;
//c = x; //x的值赋值给了c,c依旧是a/b的别名
cout << a << " " << b << " " << c << " " << d << endl;//1
return 0;
}
注意:
- 引用类型必须和引用实体是同种类型的
- 一个变量可以有多个引用(一个人可以有许多别名)
- 引用在定义时必须初始化(给一个人取别名时他必须要有大名)
- 引用一旦引用一个实体,再不能引用其他实体(b是a的别名,就不能再是其它实体的别名了)
5.1引用的使用场景
5.1.1引用做参数(输出型参数)
输入型参数只传参使用,而输出型参数传参->形参的改变会影响实参,比如经典的Swap交换函数。
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
5.1.2引用做参数(减少拷贝,提高效率)
以值作为参数进行传参时,由于形参是实参的临时拷贝,要开辟额外的空间,尤其当参数非常大时,效率就更低,而引用传参是对自身进行相关操作,从而提高了效率。
#include <time.h>
struct A
{
int a[10000];
};
void TestFunc1(A a)
{
}
void TestFunc2(A& a)
{
}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
5.1.3引用做返回值(减少拷贝,提高效率)
传值返回,函数销毁时会把要返回的值拷贝到寄存器中保存(出了作用域函数销毁,空间返回给了操作系统,所以要把返回的值先保存到寄存器中),外面接收时再将寄存器里的值拷贝出去;
而引用做返回值时,返回的是这个值的别名(自己本身),减少了拷贝,提高了效率,但引用做返回值有要求:出了函数作用域,对象不在了(空间回收了),就不能引用返回,还在就可以引用返回。
- 传值返回情况1:变量n创建在静态区
- 传值返回情况2:变量n创建在栈区
值得注意的是:编译器并不会因为变量创建在哪个区域上而改变返回时的方式,编译器采用傻瓜时的处理方法,都会将其拷贝到临时变量中用于返回 - 传引用返回(正确的引用返回)
变量创建在静态区,当Count函数结束作用域销毁后,n依旧存在,将n的别名(自己)传给ret。 - 传引用返回(错误的引用返回)
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用。
引用返回,如果已经还给系统了,则必须使用传值返回。
总结:
1.引用传参基本上在任何场景都可以使用
2.出了函数作用域,对象不在了(空间回收了),就不能引用返回,还在就可以引用返回。
5.1.4引用做返回值(修改返回值+获取返回值)
返回值的本身,用于进行获取或者修改
#include<iostream>
using namespace std;
#include<assert.h>
struct SeqList
{
int a[100];
size_t size;
};
int& SLAt(SeqList* ps, int pos)
{
assert(ps && pos >= 0);
return ps->a[pos];
}
int main()
{
SeqList s;
SLAt(&s, 0) = 1;//0处赋值
cout << SLAt(&s, 0) << endl;//获取返回值
SLAt(&s, 0) += 5;//修改
return 0;
}
5.2常引用
int main()
{
//不可以,
//引用过程中,权限不可以放大
const int a = 10;//a被const修饰后具有常性
//int& b = a;//常量不能通过取别名变成变量
// 可以,c拷贝给d,没有放大权限,因为d的改变不影响c
const int c = 20;
int d = c;
//可以
//引用过程中,权限可以平移或者缩小
int x = 0;
int& y = x; //权限平移(int->int)
const int& z = x;//权限缩小(int->常量)
x++;//可以
//z++;//不可以,常量不可以修改
//可以给常量去别名
const int& i = 30;
//不同类型转换时会发生截断或者提升(截断或者提升生时生成临时变量,临时变量具有常性)
double dd = 1.1;
int ii = dd;//dd生成一个int类型的临时变量拷贝(赋值)给ii
//int& n = dd;//不可以,权限放大(常量->int)
const int& n = dd;//可以,权限平移(常量->常量)
return 0;
}
总结:
- 在引用过程中,权限可以平移或者缩小,但不可以放大。
- 不同类型转换时会发生截断或者提升,截断或者提升生时生成临时变量,临时变量具有常性。
5.3引用和指针的区别
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
下引用和指针的汇编代码对比:
语法层面:引用没有开辟空间,和引用实体共用一块空间;指针开辟了空间,存储a的地址。
底层层面:引用是按照类似指针的方式实现的。
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
6.内联函数(inline)
6.1宏函数的正确写法以及优缺点
在C语言中,如果一些短小的函数被频繁的调用,那么将会不停的建立栈帧,导致效率的降低,那么这时候将会用宏函数去替换这些短小的函数被频繁的调用的函数
#include<iostream>
using namespace std;
//#define Add(int x,int y) return (x+y);//错误
#define Add(x,y) ((x)+(y))//正确
int main()
{
int x = 10, y = 20;
int ret = Add(x, y);
cout << ret << endl;
return 0;
}
错误点:
- 宏函数的参数不能有类型
- 宏函数不能用return
- 宏函数后面没有’ ; ’
因为宏函数是用于在预处理阶段完成替换,直接替换到代码中,不能有return、类型和’ ; '。
宏的优缺点?
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
6.2内联函数
在C++中,为了解决大量重复调用的函数,使用了内联函数去解决。
以inline修饰的函数叫做内联函数(inline+函数),编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int x = 2;
int y = 3;
int ret = Add(x, y);
cout << ret << endl;
return 0;
}
内联函数的特性:
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:形成代码膨胀,可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。所以内联函数的声明和定义要写到一块(.h文件中)
7. auto关键字
7.1 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替换为变量实际的类型。
7.2 auto的使用规则
- 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;
*a = 20;
*b = 30;
c = 40;
return 0;
}
- 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
7.3 auto不能推导的场景
- auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
2.auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
8.范围for循环
8.1范围for循环语法
for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int main()
{
//适用于数组
// 范围for 语法糖
// 依次取数组中数据赋值给e
// 自动迭代,自动判断结束
// for (int x : arr)
int arr[] = { 1,3,4,5,6,7,8,9,0,3,4,5 };
//打印
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
//修改
for (auto& e : arr)//注意修改要取别名
{
e *= 2;
}
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
8.2范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
数组传参传的是指针,不是数组,所以for的范围不确定
- 迭代的对象要实现++和==的操作。
9.指针空值nullptr(C++11)
在C语言中,NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,为了解决这一麻烦,在C++中使用nullptr表示指针空值。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。