目录
C++关键字
C++一共有63个关键字,这里先全部列举出来,不做详解。
命名空间
为什么要使用?
一个程序的开发往往由多人合作完成,在此过程中,函数名、变量名、类名是大量出现的,这就很有可能出现命名重复的情况。那么为了避免命名冲突,C++使用命名空间对标识符的名称进行本地化。
这里概括下命名空间的好处:
避免命名冲突。一个命名空间就定义了一个新的作用域,里面的内容被局限在这个域中,从而与其他模块or库中的同名实体隔离开,避免了命名冲突。
组织和管理代码。相关的实体被放置在同一个命名空间中,可以更好地组织和管理代码,代码结构清晰可见。
提供代码封装:命名空间提供了代码的封装性,使得实体的访问范围更加清晰、可控。
如何定义一个命名空间?
namespace关键字+命名空间的名字+{ }
{ }中为命名空间的成员,可以是变量、函数、自定义类型、命名空间(命名空间可以嵌套)
同一个工程中允许存在相同名称的命名空间,编译器会把名字相同的命名空间合到一起。
同一个命名空间域中不能定义同名的,不然会冲突。不同命名空间域可以。
如何使用?
-
加命名空间名称以及作用域限定符
#include<stdio.h> namespace A { int a=0; int b=0; } int main() { printf("%d",A::a); return 0; }
当你使用某个命名空间的变量时,编译器不会默认去那块空间找,必须你指定变量的命名空间。
格式:命名空间名+ :: +成员名称
-
用using将命名空间中的某个成员引入
如果某个成员是常用的,那每用一次都要带上命名空间,就会很麻烦。这时,可以把常用的成员引入进来,这样就可以直接使用了。
注意看这个例子是如何引入a和b的:
#include<stdio.h> namespace A { int a=0; int b=0; } using A::a; int main() { printf("%d %d",a,A::b); return 0; }
格式:using+命名空间名+::+成员名称
这种方式相当于把a单独展开在全局域中,a可以在其他任何地方被使用。这种方式既能展开常用的成员,又不会暴露整个命名空间。
-
使用using namespace命名空间名称引入
如果你在使用命名空间A中的任何变量时,都不想加命名空间名称的话,可以把整个命名空间都展开,这样其中的任何成员你都能直接使用了。
#include<stdio.h> namespace A { int a=0; int b=0; } using namespace A; int main() { printf("%d %d",a,b); return 0; }
格式:using namespace +命名空间名称
引入命名空间A后,在使用a的地方,编译器会先在函数的局部域中找,然后去全局域找,没找到的话再去展开的命名空间域A中寻找a。
我们看到的“using namespace std; ”,表示在程序中引用了 C++ 标准库中的命名空间 std。这样我们在使用标准库中的各种类和函数时,就不用在前面加上 std::。 如: cin, cout可以直接使用而不需要std::cin, std::cout。
注意:using namespace引入命名空间的方式虽然方便,但也使命名空间失去了它的封装和保护作用,你定义的内容暴露了出来,就会有重复命名的风险。所以,我们一般只在练习中使用,写项目时,还是尽量使用前两种方法。
C++输入与输出
C++版的hello world:
#include<iostream>
using namespace std;
int main()
{
cout<<"hello world"<<endl;
return 0;
}
-
对应于C语言的printf和scanf,C++使用cout标准输出 和cin标准输入。后者要更加方便,因为不需要再指定格式了,cout和cin,可以自动识别变量类型。'
C:
int a;
scanf("%d",&a); //得指定格式
C++:
cin>>a; //自动识别a的类型,更方便
-
endl是特殊的C++符号,表示换行。cout、cin和endl都被包在<iostream>中,所以使用时必须包含<iostream>头文件,并使用标准库的命名空间std。
补充了解:头文件<iostream>和命名空间std之间,有什么关系?为什么两者都要写进程序?
<iostream>
是C++标准库中的头文件,用于输入输出操作。而std
是C++标准库中定义的命名空间,包含了很多与输入输出相关的类和函数。在C++程序中,我们可以通过包含头文件
<iostream>
来引入输入输出的功能,例如输入流std::cin
和输出流std::cout
。这些输入输出对象都位于std
命名空间中,所以在使用它们之前,我们需要使用using namespace std;
语句或者在每个对象前加上std::
前缀来指明它们所属的命名空间。因此,
<iostream>
和std
之间的关系是,<iostream>
头文件提供了输入输出功能的声明,而std
命名空间则包含了具体的实现。通过这种方式,我们可以使用C++标准库提供的输入输出功能。 -
<<是流插入运算符,>>是流提取。
-
C++的头文件不带.h
-
如何展开使用std? 在日常练习中,直接用using namespace std; 。在代码多的项目中,只展开常用的那部分。如:using std::cout;
缺省参数
缺省参数是C语言所没有的,C++新增的语法功能。我们在声明or定义函数时,为函数的参数指定一个缺省值。在调用该函数时,如果相应位置没有传实参,那么就采用该形参的缺省值,否则使用指定的实参。
缺省值就像备胎。当有实参传过来时,用不上它;当没有实参传过来时,才会用它。
全缺省参数
函数的全部参数都给出了缺省值。
#include<iostream>
using namespace std;
void Func(int a=0,int b=0,int c=0)
{
cout<<a;
cout<<b;
cout<<c;
}
int main()
{
Func(); //0 0 0
Func(10); //10 0 0
Func(10,20); //10 20 0
Func(10,20,30); //10 20 30
Func(10,,30); //不能这么写!要从左往右依次给出
return 0;
}
半缺省参数
仅部分参数给出了缺省值。
#include<iostream>
using namespace std;
void Func(int a,int b=0,int c=0)
{
cout<<a<<b<<c;
}
int main()
{
Func(); //0 0 0
Func(10); //10 0 0
return 0;
}
注意事项
注意:
半缺省参数必须从右往左连续给出,不能间隔着给。
void Func(int a=10,int b,int c){} //× void Func(int a=10,int b,int c=30){} //× void Func(int a,int b=20,int c=30){} //√
缺省参数不能在函数定义和声明中同时出现。因为如果两边给出的缺省值不一样,那编译器不知道该采取哪个。我们一般就写在声明中,定义中不写。
缺省值必须是常量or全局变量。
C语言不支持。
应用实例
我通过一个实例来展现缺省参数的好处:
//原本给Stack初始化: void StackInit(Stack*ps){ int newCapacity=ps->capacity==0?4:2*(ps->capacity); ps->a=(int*)malloc(newsCapacity*sizeof(int)); …… } //思路是:如果原本容量为0,那么第一次就开4个单位的空间,否则就扩容为之前的两倍 //但,假如我们初始化时就要开400个空间,那要从开4个空间开始,两倍两倍地慢慢扩容,直至400个。这种方式会不断调用函数、建立栈帧,大大降低了运行效率。
引入缺省参数: void StackInit(Stack*ps,int capacity=4){ ps->a=(int*)malloc(capacity*sizeof(int)); } //思路:当我不传参,函数会默认开4个空间。当我初始化时想开400个空间,那直接传400即可。
可见,使用缺省参数可以简化代码,提高代码的可读性。
这里罗列了缺省参数的优点:
简化函数调用:使用缺省参数,函数调用时可以省略一些常用的参数,使函数调用更简洁明了。
增加函数的灵活性:在函数定义中使用缺省参数可以使函数更加灵活。调用者可以选择性地提供不同的参数值,根据实际需求对函数进行定制。
向后兼容性:在已经使用该函数的代码中,如果新增了一个参数并且为其设置了默认值,不影响之前的函数调用。因此,使用缺省参数可以确保向后兼容性,避免因为新增参数导致调用错误。
函数重载
为什么要有函数重载?
C语言中,如果功能相似但是形参类型不同,那就得写不同的函数来实现:
int Add1(int a,int b);
double Add2(double a,double b);
double Add3(int a,double b);
这些功能很相似的函数却要取不同的名字来让编译器区分。程序员在调用时,可能也会眼晕,相似的函数名会带来不便。C++对此做出了改进,让功能相似的函数名称统一为一个,这样使用时就很方便,像在用同一个函数一样(但实际上构成重载的函数是不同的),也减轻了记忆函数名的负担。
用函数重载:
int Add(int a,int b); double Add(double a,double b); double Add(int a,double b);
为什么是函数重载?
同一个作用域内,多个函数名相同,但形参列表不同(参数个数or参数类型or类型顺序不同)的函数,我们叫做函数重载。常用于处理功能类似但数据类型不同的问题。
注:
返回值不同不构成重载。函数重载是通过形参列表区分的,和其他无关。
main函数不能重载,因为程序的入口只能有一个。
C语言没有函数重载。
实现函数重载后,我们调用函数传不同的参数时,编译器会根据我们传递实参的类型和个数,判断要调用哪个ADD函数。对调用者来说,就方便多了。
引用
概念
生活中,我们会有朋友取一些绰号,本名也好,绰号也好,实际指的都是同一个人。引用就像是变量的“绰号”,它不是新定义了一个变量,而是给已存在变量取了一个别名,它并未开辟新的内存空间,它和它引用的对象共用同一块内存空间。
格式:类型 & 引用变量名(对象名) = 引用实体
例:
int a=0;
int&b=a; //b实际上就是a那块空间。a是int 类型,所以b也是int类型
//这里只开了一块空间a(又名b)
区别于这个例子:
int a=0;
int b=a; //这里开了两块空间,分别叫a和b,并且把a的值赋给了b
特性
-
引用在定义时必须初始化。
-
一个变量可以有多个引用。
-
引用一旦引用一个实体,就不能再引用其他实体。
int a=0; int&b=a; int c=0; b=c; //× b已经引用a那块空间了,不可以再更改
使用场景
-
做参数
-
输出型参数
如,我们用c语言写交换函数:
void Swap(int*a,int*b){ int tmp=*a; *a=*b; *b=tmp; }
用引用写,直接操纵那片空间:
void Swap(int& a,int& b){ int c=a; a=b; b=c; }
-
大对象传参,提高效率
比如:实参占4万个字节,如果用形参拷贝的方式,得另开空间。而用引用则可以节省空间,并提高效率。
-
-
做返回值
注:不是所有情况下都能用引用做返回值!
引用做返回值的前提:返回对象在出了函数作用域,依然继续存在。如:静态变量
对比这两组:
错误示例:
int& Count()
{
int n=0;
n++;
return n; //×,出了作用域,n就销毁了,那么一定不能用引用返回,得传值
}
int main()
{
int ret=Count();
}
正确示例:
int& Count()
{
static int n=0;
n++;
return n; //√ 出作用域,n未销毁,可以传引用
}
int main()
{
int ret=COunt();
}
引用和指针的不同?
引用在概念上定义一个变量的别名,指针则是存储一个变量的地址。
引用在定义时必须初始化,指针没有要求。
引用在初始化引用一个实体后,就不能再引用其他的实体,而指针可以在任何时候指向任何一个同类型的实体。
没有NULL引用,但有NULL指针。
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是4或8个字节。
引用自加即引用的实体+1,而指针则是向后偏移一个类型的大小。
有多级指针,但没有多级引用。
访问实体的方式不同,指针需要显示解引用,引用编译器自己处理。
引用比指针用起来更安全。
内联函数
当短小的函数被频繁调用,如快速排序中频繁地调用Swap函数达十万次,在这期间不断地创建、销毁栈帧,这就导致了效率变低。在C语言中,针对这种情况,我们会用宏函数来写;在C++中,我们用更优的内联函数来处理。
概念
以inline修饰的函数叫内联函数。编译时会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,能够提升效率。
特性
-
是种以时间换空间的做法。若编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。
注:较长的函数是不会展开的,不然占很多内存。
-
inline于编译器而言只是个建议,编译器未必会采纳。一般将函数规模较小、不是递归且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。(编译器会自动优化)
-
inline时,不要将声明和定义分离,分离会导致链接错误。因为内联函数被展开,编译时无法在符号表上保存其有效地址,链接时就不能根据符号表上的地址找到函数,所以会链接不到。所以,内联函数的定义就直接写进.h文件。
auto关键字
当类型名很长时,我们用auto替代。auto可以自动判断变量类型。
注:使用auto时,必须要初始化。因为编译器会在编译阶段根据初始化表达式来推导auto的类型,且编译器会在编译阶段将auto替换为变量实际的类型。
使用细则
-
auto与指针、引用结合起来用时
用auto声明指针类型时,auto和auto*没有区别。
int x=10; auto a=&x; auto*b=&x; //两者没有区别,只是后者强调了b是个指针而已。
用auto声明引用类型时,必须加上&。
int x=0; auto& c=x;
-
在同一行定义多个变量时,这些变量须是相同的类型。
auto a=1,b=1; // √ auto a=3,b=3.0; // ×
不能用的场景
-
不能做函数的参数
int Add(auto a,auto b){} //×
因为编译阶段就要将auto换成形参实际的类型,而此时实参没有传过来,编译器无法对形参的实际类型做推导。
-
auto不能直接用来声明数组
auto arr[]={1,2,3}; //×
基于范围的for循环
对于一个有范围的集合而言,由程序员来写明循环的范围 是多余的,有时候还易犯错,因此,C++引入了基于范围的for循环。
格式:for(范围内用于迭代的变量:被迭代的范围)
#include<iostream> using namespace std; void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for (auto e : array) { cout << e ; } } int main() { TestFor(); return 0; }
使用条件:
for循环迭代的范围必须是确定的。
对于数组,应为第一个和最后一个元素的范围。
对于类,应提供begin和end的方法,begin和end就是范围。
指针空值---nullptr
NULL实际上是一个宏,它可能被定义为字面常量0或者void*,这可能会导致一些麻烦,因而C++引入了nullptr。
-
nullptr是一个关键字,因而在使用时不用引头文件。
-
sizeof(nullptr)与sizeof((void*)0)所占字节数相同。
-
相比NULL,建议用nullptr,不易出错,提高代码的健壮性。