本篇是作为C++学习的第一篇博客,主要讲解一些入门知识,为后续学习打基础。
C++是在C的基础上,容纳了面向对象编程思想,并增加了许多有用的库以及编程范式等。因此C++是完全兼容C的,C代码不经过任何修改即可在C++编译器下编译运行。
本篇博客的主要目标是:补充C语言语法的不足,掌握C++是如何对C语言设计不合理的地方进行优化的。
一、C++关键字
C语言有32个关键字,C++98有63个关键字(见下图),C++11有73个关键字。
这里不对其详解,需要用到时再细讲。
二、命名空间
命名空间的出现是为了解决命名冲突的问题。在C语言中,全局域内不允许出现完全相同的标识符,而在大型工程中难免会有同名现象,这带来了很大的不便,因此C++出现了命名空间,将相同的标识符放在不同的命名空间里就可避免冲突。
1.定义
命名空间的定义需要使用关键字namespace,后面跟命名空间的名字,再接一对{ }即可。命名空间内可以定义变量、函数、结构体。
注意:命名空间的定义没有‘;’,要与结构体的定义区别开来。
namespace name
{
int rand = 10;//变量
int Add(int left, int right)//函数
{
return left + right;
}
struct Node//结构体
{
struct Node* next;
int val;
};
}
命名空间支持嵌套:
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;
}
}
}
同一项目中允许存在多个相同名称的命名空间,编译器会自动将它们合并成同一个命名空间。
2.使用
一个命名空间相当于定义了一个新的作用域,若想使用里面的标识符,必须指明命名空间。
方法有三:
①在标识符前加命名空间名称和作用域限定符(: :)
int main()
{
printf("%d\n", name::rand);//::为作用域限定符
return 0;
}
②使用using将命名空间里的成员引入
using name::rand;
int main()
{
printf("%d\n", rand);
return 0;
}
③使用"using namespace 命名空间名称"将命名空间展开
using namespce name;
int main()
{
printf("%d\n", rand);
Add(10, 20);
return 0;
}
3.std
std是C++标准库的命名空间,库里的各种接口函数均在该命名空间里。
在写代码时经常会用到库里的标识符,为了方便,在平常练习中,可以展开该命名空间,这样可以直接使用而不用加作用域限定符。但在大型工程中一般不建议这样使用,因为大型工程标识符众多,可能会与标准库的标识符冲突。
usinng namespace std;
三、C++输入、输出
C++也有一套自己的输入输出方案:
#include<iostream>
using namespace std;
int main()
{
int a=0;
cin>>a;//输入
cout<<"Hello world!!!"<<endl;//输出
return 0;
}
说明:
①使用cin和cout时,必须包含头文件#include<iostream>。上面代码中,将std展开了,因此可以直接使用;如果不展开,需要加作用域限定符std::cin、std::cout
②在这里,>>是流提取运算符,<<是流插入运算符
③endl是特殊的C++符号,表示换行,与\n作用相同,也包含在iostream头文件里
④使用C++的cin/cout比C语言的scanf/printf更方便,不需要控制格式,它们可以自动识别变量类型
⑤(该点对于初学者可能不懂,学习了类与对象就明白了)cin和cout实际上分别是istream和ostream类的对象。在C语言里<<和>>分别是左移位和右移位操作符,这里涉及到了运算符重载的知识。
四、缺省参数
1.概念
缺省参数是在声明或定义函数时,为函数参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
void Func(int a = 0)
{
cout<<a<<endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值0
Func(10); // 传参时,使用指定的实参10
return 0;
}
2.分类
①全缺省参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
②半缺省参数
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
3.注意
①半缺省参数必须从右往左依次给出,不能间隔。(即所有半缺省参数必须放在普通参数的后面)
②缺省参数不能在声明和定义时同时出现。一般推荐出现在声明中。
③缺省值必须是常量或全局变量
④C语言不支持缺省参数
五、函数重载
1.概念
函数重载是函数的一种特殊情况,C++允许在同一作用域内声明多个功能相近的同名函数,这些函数的形参列表不同(类型不同、数量不同、类型数量相同但顺序不同),常用来处理功能类似而数据类型不同的问题。
注意:函数重载只对函数名和形参列表有要求,与返回值无关。因此,当两个函数的函数名和形参相同而返回值不同时,是不构成重载的。
2.原理——名字修饰
为什么C++支持函数重载而C语言不支持呢?
原因是C++编译器会对函数名进行修饰,比如g++会将函数名修饰为“_Z+函数长度+函数名+类型首字母”,这样构成重载的函数经过修饰后其函数名就会变得不一样,在链接阶段就可以根据调用情况而选择对应的函数。但C语言编译器不会对函数名进行修饰,导致同名函数在链接时无法区分,进而报错。
六、引用
1.概念
int main()
{
int a = 10;
int& ra = a;//定义引用变量ra
printf("%p\n", &a);
printf("%p\n", &ra);
}
引用是为已经存在的变量取一个别名,这个别名也被叫做引用变量。引用变量的定义需要使用&。例如上述代码中,引用变量ra就是a的引用。
编译器不会对引用变量开辟内存空间,它和被引用的变量共用同一块内存空间。因此,上述代码的打印结果是一样的。
注意:引用变量和引用实体必须是同类型的。
2.引用的特性
①引用变量在定义时必须初始化
②一个变量可以有多个引用
③引用变量一旦定义就不可改变引用实体
3.常引用
常引用就是在引用时加const修饰符。常引用表示引用变量本身是只读的,即无法通过引用变量修改变量的值。
因此,常引用变量可以对const修饰的变量或常量进行引用。此外,常引用变量还可以对普通变量(没有被const修饰的变量)进行引用。但是,普通引用变量是不能对const修饰的变量或常量进行引用的(原因:const修饰的变量或常量本来是只读的,但普通引用变量可读可写,这样通过引用变量这个别名就可以修改变量的值,显然是不合理的。)。为了方便记忆,可以理解为:权限只能平移和缩小,但不能扩大。
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 错误,权限扩大
const int& ra = a; // 正确,权限平移
// int& b = 10; // 错误,权限扩大
const int& b = 10; // 正确,权限平移
double d = 12.34;
double& rd1 = d; // 正确,权限平移
const double& rd2 = d; // 正确,权限缩小
}
4.使用场景
①做形参
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
例如上述代码中的交换函数,在C语言中要想实现两个数的交换必须传指针,而C++里形参可以使用引用,实参不需要指针,直接传递变量即可。可见,引用的使用让传参更加方便。
②做返回值
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
当引用做返回值时,函数会返回变量的引用(也即别名,至于具体的名字是什么,不用关心,这是编译器的工作)。因此,使用引用返回时,返回的变量必须是全局变量或静态局部变量,否则在函数栈帧销毁后局部变量不复存在,返回其引用会带来未知错误。
补充说明:当函数使用值返回时,函数会返回变量的一份临时拷贝,虽然这时可以返回局部变量,但是效率是非常低下的,尤其是当返回值类型很大时,效率会更低。因此,优先使用引用返回,效率更高。
5.引用VS指针
在语法概念上,引用只是一个别名,没有独立空间,和其引用实体共用一块内存空间;而指针通常是指指针变量,用来存放地址,独自占用空间,大小为4或8个字节。
在底层实现上,引用其实是占用空间的,因为引用是按照指针的方式来实现的。
引用和指针的不同点:
①引用在定义时必须初始化;指针没有要求
②引用在初始化时引用一个实体后,就不能再引用其他实体;而指针变量可以随意修改指向
③没有空引用;但有空指针
④有多级指针,但没有多级引用
⑤引用比指针使用起来相对更安全
七、内联函数
1.概念
以inline修饰的函数叫做内联函数。编译阶段编译器会在调用内联函数的地方将其展开,用函数体替换函数的调用,没有函数调用建立栈帧的开销,可以提升程序的运行效率。
2.特性
①inline是一种以空间换时间的做法,虽然减少了调用开销,提高了效率,但是可能导致代码膨胀,使目标文件变大
②内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
③inline修饰的函数其声明和定义必须在一起,不能分离(这里的分离指的是声明和定义在不同的源文件里)。为什么呢?首先要知道,inline函数在编译阶段就会展开,因此只有在其定义的源文件中可以成功展开,而且inline函数是不会进入符号表的。当声明和定义分离时,声明放在头文件中,定义放在源文件1中,这时另一个源文件2包含该头文件想要使用该内联函数时,当头文件展开后,只有声明,没有定义,编译阶段无法展开,依旧保持函数调用的格式。到了链接阶段,由于符号表中没有该内联函数,无法成功链接,报错——“无法解析的外部符号”。(如果还是理解困难,可以参考博客【程序环境和预处理】中的编译过程)
④C++规定:在类定义中定义的成员函数都是内联函数(即使不加inline也是)
八、auto关键字(C++11)
1.新的含义
随着程序越来越复杂,用到的类型也越来越复杂,有的类型冗长难于拼写,若多次使用还需拼写多次,非常不方便。比如后面将会学到的map迭代器的类型:
std::map<std::string, std::string>::iterator it = m.begin();
如何解决上述问题呢?一种方法是使用typedef进行类型重定义:
typedef std::map<std::string, std::string> Map_iterator;
这种方法虽然可行,但终究还是要写一次类型,能否做到一次都不用写呢?因此C++11给auto赋予了新的含义。
在早期的C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但是由于它可以省略,一直没有人使用它。
于是,在C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量的类型必须由编译器在编译期间自动推导而得。
需要注意的是,使用auto定义变量时必须对其进行初始化,这样编译器才有了推导依据——根据初始化表达式来推导变量的类型。编译器在编译期间会将auto替换为变量的实际类型。
2.使用细则
①用auto声明指针类型时,用auto和auto *没有任何区别;但是用auto声明引用类型时必须加&
②使用auto在同一行声明多个变量时,这些变量必须是相同类型,否则编译器会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
③auto不能作为形参类型,因为编译器无法对其实际类型进行推导,即使有缺省值也不行
④auto不能用来声明数组
⑤为了避免与C++98中的auto发生混淆,C++11中只保留了auto作为类型指示符的用法
⑥auto在实际中最常见的优势用法是跟C++11中新增的新式for循环和lambda表达式进行配合使用
九、基于范围的for循环(C++11)
1.语法
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()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
}
注意:与普通循环类似,可以用continue来结束本次循环,用break来退出循环
2.使用条件
①for循环迭代的范围必须是确定的:
对于数组而言,就是第一个元素和最后一个元素之间;对于容器而言,必须提供begin和end方法,begin和end就是循环迭代的范围。
例如,下面的代码就是有问题的,虽然形参表面上是数组,但实际是一个整型指针,从而导致for的范围不确定:
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
②迭代的对象要实现++和==操作
十、空指针nullptr(C++11)
既然有了NULL,为什么C++11还要引入nullptr呢?究其原因还是比较复杂的,限于篇幅,将在后续博客【】进行解释,这里暂时记住即可:在支持C++11的编译器下写代码时用到空指针最好使用nullptr
注意:使用nullptr时不需要包含头文件,因为nullptr在C++11中是作为新的关键字引入的