目录
1.C++中的关键字(了解)
2.命名空间
2.1 什么是命名空间
在C++中,变量、函数、类都是大量存在的,这些变量、函数和类的名称都存在与全局作用域中,这样就会产生很多冲突。例如:在软件开发过程中,一个软件产品大多都是由多个开发人员协作完成的,这样就有可能不同的开发人员使用了相同的变量名、函数名以及类名等。为了解决这些问题,C++中引入了命名空间的概念。
使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。
命名空间在C++程序中的使用:
2.2 命名空间的定义和使用
2.2.1 定义命名空间
命名空间的定义需要使用namespace关键字,同时命名空间可以嵌套定义、一个文件中也支持多个同名的命名空间(编译时会自动合成为一个命名空间)。
1)基本格式
namespace namespace_name{//命名空间成员}
2)几种定义格式举例
//1. 普通的命名空间namespace N1 // N1 为命名空间的名称{// 命名空间中的内容,既可以定义变量,也可以定义函数int a ;int Add ( int left , int right ){return left + right ;}}//2. 命名空间可以嵌套namespace N2{int a ;int Add ( int left , int right ){return left + right ;}namespace N3{int b ;int Sub ( int left , int right ){return left - right ;}}}//3. 同一个工程中允许存在多个相同名称的命名空间 , 编译器最后会合成同一个命名空间中。namespace N1{int Mul ( int left , int right ){return left * right ;}}
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
2.2.2 命名空间的三种使用方法
1)加命名空间名称及作用域限定符
int main(){
cout<<N1::a<<endl; //每次使用都需要加命名空间名作用域限定符
return 0;}
2)使用using将命名空间中成员引入
using N1::a; //只有通过这种方式引入的变量或函数等才能直接使用,如果要使用该命名空间中的其他变量或函数还需采用1)中的方式使用
int main(){
cout<<a<<endl;
return 0;}
3)使用using namespace 命名空间名称引入
using namespace N1; //直接将命名空间中的所有变量、函数等引入,可以随便使用(不建议这样做)。
int main(){
cout<<a<<endl;
return 0;}
3.C++的输入和输出
3.1 C++标准输入输出
在C++中cout是标准输出、cin是标准输入,在使用cout标注输出和cin标准输入时需要包含<iostream>头文件和std标准命名空间。
int main(){
cout<<"hello world"<<endl; //控制台输出hello world并换行
return 0;}
3.2 C++标准输入输出的使用
1)不需要控制输出格式
using namespace std;
int main(){
int a = 10;
cout<<a<<endl; //10,不需要控制输出格式,自动确定输出格式
cout<<"hello C++"<<endl; //加双引号输出字符串
return 0;}
2)多变量输出
using namespace std;
int main(){
int a = 10;
char ch = 'C';
cout<<"a = "<<a<<" ch = "<<ch<<endl; //一个输出可以输出多个变量,不同变量不需要控制输出格式
return 0;}
3)endl换行
using namespace std;
int main(){
cout<<"hello world"<<endl; //endl自动换行,字符串中也可以使用'\n'换行
return 0;}
4)“陷阱”
using namespace std;
int main(){
char a = 'c';
char* b = &a;
cout<<a<<endl; //输出c...随机值,b为char*存储的是变量a的地址输出b应该为a的地址,但是c++标准输出中默认将char*按字符串处理,因此访问了a的内容。
return 0;}
4.缺省参数和函数重载
4.1 缺省参数
4.1.1 缺省参数的概念
缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。注意:缺省参数不能在定义和声明时同时定义,只需要在函数声明时指定缺省参数值就可以。
//.h文件
int Add(int a = 1,int b = 1)
//.cpp文件
int Add(int a ,int b){
return a+b;}
//main函数
int main(){
Add(); //不指定参数调用函数
Add(10); //指定部分参数值,默认从左向右指定
Add(10,10); //指定全部参数值
}
4.1.2 缺省参数的类型
1)全缺省参数
函数的所有参数全为缺省参数称为全缺省参数。
void testFunc(int a = 1,int b = 1,int c = 1){
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
2)半缺省参数
函数的参数中,部分参数为缺省参数称为半缺省参数。半缺省参数中,缺省的参数必须是右侧的参数且不允许缺省参数和非缺省参数间隔出现同时缺省值必须是常量或者全局变量。
void testFunc(int a,int b = 1,int c = 1){
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
4.2 函数重载
4.2.1 函数重载的概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 不同类型的的出现顺序(如果参数类型相同顺序不同也不行))必须不同,常用来处理实现功能类似数据类型不同的问题。
int Add(int a,int b){
return a+b;}
//类型不同
double Add(double a,double b){
return a+b;}
//参数个数不同
int Add(int a,int b,int c){
return a+b+c;}
4.2.2 extern "c"
有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern "C",意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。
extern "c" int add(int a,int b); //将add函数按照C语言规则编译
4.2.3 面试题
1)为什么C++支持函数重载而C语言不支持函数重载?
在C/C++语言中,一个.c/.cpp文件要生成可执行文件需要经过预处理、编译、汇编、链接几个阶段。
实际我们的项目通常是由多个头文件和多个源文件构成,而通过我们C语言阶段学习的编译链接,我们可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。因此链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就会到b.o的符号表中找Add的地址,然后链接到一起。那么链接时,面对Add函数,连接器会使用哪个名字去找呢?这里每个编译器都有自己的函数名修饰规则。由于Windows下vs的修饰规则过于复杂,而Linux下gcc的修饰规则简单易懂,下面我们使用了gcc演示了这个修饰后的名字。
通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
结论:在c++中,编译器将c++程序中多个文件链接到一起时,对函数使用了【_Z+函数长度+函数名+类型首字母】修饰规则,使得相同的函数名在参数不同时有可以找到不同的函数地址。而在C语言程序中直接使用函数名,不会对函数名做任何修饰,因此如果存在同名函数,编译器不知道什么时候调用的那个函数。综上,c++支持函数重载,c语言不支持函数重载。
2)下面两个函数能构成函数重载嘛?
int add(int a,int b){
return a+b;}
int add(int a = 0,int b = 0){
return a+b;}
答案:不构成,构成函数重载的条件是函数名相同,参数的类型、数量、顺序等不同,在这两个函数中,参数的数量和类型完全相同,唯一的区别是一个使用缺省参数一个没有使用,而缺省参数不能作为函数重载的条件。
3)C++中能否将一个函数按照C的风格来编译?
答案:能,使用extern "c"
5.引用
5.1 引用概念
c语言中指针类型实现了多个变量访问同一个地址,通样C++中有引用类型实现了“一个变量多个名称”,即多个变量访问一个地址空间。
注意:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
5.2 引用的定义和特性
5.2.1 定义引用变量
格式:类型& 引用变量名(对象名) = 引用实体
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);//引用变量不会单独开辟空间,因此输出地址都是一样的
printf("%p\n", &ra);
}
注意:引用类型必须和引用实体是同种类型的
5.2.2 引用的特性
1)引用的特性
1. 引用在定义时必须初始化(因为引用变量编译器不会给开辟空间,因此要求定义时必须初始化)
2. 一个变量可以有多个引用
3. 引用一旦引用一个实体,再不能引用其他实体
void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}
2)常引用
void TestConstRef()
{
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const double& rd = d;
}
5.3 引用的使用
5.3.1 引用类型做参数
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
5.3.2 引用类型做返回值
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
注意:如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回、
5.3.3 传值和传引用性能分析
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低的,尤其是当参数或者返回值类型非常大时,效率就更低。引用实则是原变量的一个别名,传引用相当于直接将该变量作为参数或者返回值,省去了临时拷贝的系统开销。
注意:当局部变量做返回值时,如果局部变量不是静态局部变量时,不能使用该局部变量的引用作为返回值进行返回;同时,传参时我们只希望将参数值传过去不希望改变参数时将原变量值进行改变,这时也不能使用引用作为函数的参数。
5.4 引用和指针的区别
从语法层面理解,引用是变量的别名,引用变量不会开辟空间存储。从底层实现的角度,引用是使用指针实现的,也会想指针一样开辟空间。
1)引用在定义时必须初始化,指针可以先定义后初始化。
2)引用定义并初始化后,引用对象不能在改变;指针指向一个变量后还可以改变指向其他变量。
3)引用对象不能为NULL;指针可以指向NULL
4)sizeof一个引用变量时,结果为变量类型;sizeof一个指针是=时,32位系统下始终是4个字节,64位系统下始终是8个字节。
5)引用进行自增自减,直接对引用对象进行操作;指针进行自增自减是将地址+1或-1.
6)有多及指针但是没有多级引用。
7)引用访问实体时由编译器处理;指针访问指针对象时需要显示解引用。
8)引用比指针使用起来相对更安全。
6.内联函数
6.1 内联函数的概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。
6.2 内联函数的特性
1)inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。
2)inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。
3)inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
面试题(重点)
C语言中宏的优点和缺点是什么?c++中是如何解决宏的缺点的问题的?
答案:
宏的优点:
1.增强代码的复用性。例如:宏函数 2.提高性能。宏的处理机制是宏替换,不需要向函数变量那样压栈等系列复杂操作。
宏的缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
C++中使用内联函数代替宏函数,使用const修饰变量代替宏常量。
7.C++11新内容
7.1 auto关键字
C++11中,auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int main()
{
int a = 10;
auto b = a;//编译器自动推导出b是int类型
auto c = 'a';//字符a赋值给c时,编译器自动推导出c是字符型
cout<<typeid(b).name()<<endl; //输出int
cout<<typeid(c).name()<<endl;//输出char
}
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
auto使用需要注意的问题
1)用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
2)当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
3)auto不能作为函数的参数
4)auto不能直接用来声明数组
3)为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
4) auto在实际中最常见的优势用法就是跟C++11提供的新式for循环,还有lambda表达式等进行配合使用。
7.2 基于范围的for循环
1)语法
for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto e : array) //e的初始值为1
cout << array[e-1] << " ";
}
2)使用条件
1. for循环迭代的范围必须是确定的;对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
2. 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在大家了解一下就可以了)
7.3 指针空置nullptr
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。