文章目录
- 什么是C++
- C++发展史
- C++关键字
- 命名空间
- C++的输入&输出
- 缺省参数
- 函数重载
- 引用
- 内联函数
- auto关键字(C++11)
- 基于范围的for循环(C++11)
- 空指针nullptr(C++11)
什么是C++
- 一种面向对象的编程语言
- 基于C语言而产生,既可以进行C语言过程化的程序设计,也可以进行面向对象程序设计
C++发展史
C语言之父—丹尼斯·里奇
C++之父----本贾尼·斯特劳斯特卢普
几个关键时期
-
C++98
:C++标准第一个版本,以模板方式重写C++标准库,引入了STL -
C++11
:增加了若干特性,让C++更像一种新语言
e.g:正则表达式、基于范围for循环、auto关键字新容器、列表初始化、标准线程库等
C++关键字
C++98 下共计有63个关键字,其中有32个关键字是C89 C90标准下的关键字
命名空间
定义
表示一个标识符的可见范围
使用namespace{}
的形式定义
e.g
namespace N1
{
int a;
int Add(int left, int right)
{
return left + right;
}
}
namespace N2
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N3
{
int c;
int d;
int sub(int left, int right)
{
return left - right;
}
}
}
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
注意:
- 命名空间中的内容,既可以定义变量,也可以定义函数
- 命名空间可以嵌套使用
- 同一个工程中允许存在多个相同名称的命名空间,编译器最终会合成同一个命名空间中(所以相同名称的命名空间中不能存在相同的成员变量名称,否则会产生冲突)
- 一个命名空间就定义了一个新的作用域,命名空间中所有内容都局限于该命名空间中
存在的原因
- 对标识符的名称进行本地化,避免命名冲突
如何使用
- 加命名空间名称及作用域限定符(空间名称::变量名)
namespace N
{
int a = 10;
int b = 20;
int Add(int left, int right)
{
return left + right;
}
}
int main()
{
//1、命名空间及作用域限定符
printf("%d\n",N::a);
return 0;
}
- 使用using将命名空间中的成员引入
namespace N
{
int a = 10;
int b = 20;
int Add(int left, int right)
{
return left + right;
}
}
using N::b;
int main()
{
//1、命名空间及作用域限定符
//printf("%d\n",N::a);
//2、使用using将空间中的成员变量引入
printf("%d\n",b);
return 0;
}
- 使用using namespace 将命名空间名称引入
namespace N
{
int a = 10;
int b = 20;
int Add(int left, int right)
{
return left + right;
}
}
//using N::b;
using namespace N;
int main()
{
//1、命名空间及作用域限定符
//printf("%d\n",N::a);
//2、使用using将空间中的成员变量引入
//printf("%d\n",b);
//3、使用using namespace将命名空间名称引入
printf("%d\n",Add(10,20));
return 0;
}
总结
- 空间名称::成员变量名:每次使用成语变量的时候都要这样写
- using 空间名称::成员变量:后续使用该变量可以不加任何说明,直接使用
- using namespace 空间名称:凡是该空间内的成员变量,均不需要任何说明,可以直接使用
- 建议:在日常练习中使用3,在项目中使用1或者2,其目的是为了避免发生冲突
C++的输入&输出
问题引入—用C++输出“hello world!”
using namespace std;
int main()
{
cout << "Hello World!" << endl;
return 0;
}
相关说明
- cout:标准输出 cin:标准输入 << >> :数据流 endl:换行,相当于C中的‘\n’
- 使用cout 和cin的时候必须包含头文件和标准命名空间std
- iostream后面没有‘.h’,有‘.h’的是旧的编译器版本(VC6.0)
- C++输入输出更方便,不需要增加数据格式控制
- 建议:日常练习直接使用 using namespace std;而在写项目的时候,建议用到std里面的哪个对象,就单独引入这个对象,目的是避免引起不必要的冲突!
缺省参数
概念
声明或定义函数时为函数的参数指定一个默认值。在调用该函数时。如果没有指定实参则采用该默认值,否则使用指定的实参
e.g
#include <iostream>
using namespace std;
void testFun(int a = 100)
{
cout << a << endl;
}
int main()
{
testFun(1);
testFun();
return 0;
}
分类
全缺省参数
顾名思义,就是说函数的所有参数都给定了一个默认值
半缺省参数
千万别顾名思义,这里的半缺省并不是指一半参数,而是说参数中有部分参数给定了默认值,并且是从右向左依次规定默认值
使用注意事项
- 半缺省参数必须从右向左依次给定默认值,不能间隔
- 缺省参数不能在函数声明和定义处同时出现,一般在声明处给定默认值
- 缺省值必须是常量或者全局变量
- C语言不支持(编译器不支持)
函数重载
重载的概念
在同一作用域
中有几个功能类似的同名函数
,他们的形参列表不同
,这样的一组函数称为函数重载
理解
两同一不同
- 同一作用域:简单理解,就是在同一个{}内的函数
- 同一函数名
- 不同的参数列表:①个数不同②顺序不同③类型不同
为什么要有函数重载
- 处理实现功能类似数据类型不同的问题
名字修饰
问题:
- 为什么C++支持函数重载,而C语言不支持函数重载?
要解决这个问题,首先我们需要明确:
在C/C++中,一个程序要运行起来,需要经历预处理 、编译 、汇编、链接四个阶段
预处理阶段主要做以下工作
- 头文件展开
- 宏替换
- 条件编译
- 去注释
编译阶段则是将预处理结束后的代码转换为汇编程序
汇编阶段是将上一步生成的汇编程序转换为机器语言即二进制代码
链接则是将生成的二进制代码与库函数以及其他目标文件通过链接器,最终生成可执行程序
在链接过程中,遇到函数调用时会通过查看符号表到对应的库或者是其他声明该函数的地方去找。
而每个编译都有自己的函数名修饰规则。我们可以在Linux平台下通过gcc与g++的编译命令来查看具体的函数名修饰规则
Linux下的编译
-
C语言代码
通过gcc编译后查看汇编代码 -
C++代码
结论:
- gcc函数修饰后名字不变,而g++的函数修饰后变成【_Z + 函数长度+类型首字母】
- 在Linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中
Windows下
的名字修饰规则
和Linux相比,Windows下C++编译器对函数名字的修饰非常难以理解,但是目的都是一样的。
总结
- C语言无法支持重载,是因为函数名无法进行区分
- C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,所以支持重载。
重载的过程
- 编译器拿用户传入的参数与已经定义的函数参数对比
- 如果参数对应完全一致,直接调用该函数
- 若没有参数完全一致的重载函数,则默认对用户的参数进行隐式类型转换
- 转换之后若有适合的重载函数,编译通过。否则直接报错!
extern “C”
有时候在C++工程中可能需要将某些函数按照C的风格来编译。在函数前加extern "C"是告诉编译器,将该函数按照C语言风格来编译。
e.g
tcmalloc是Google用C++实现的一个项目,他提供tcmalloc和tcfree两个接口来使用,但是如果是C项目就无法使用这两个接口,因此使用extern "C"就可以解决。
几点注意事项:
- 在C++程序中,使用extern "C"来修饰某一个函数,其实质是告诉编译器在生成符号表是对该函数不采用C++的编译规则,而是采用 C语言的编译规则。
- 被extern "C"修饰的函数,在符号表中的名字就是该函数的函数名,不做任何修饰
- 被extern "C"修饰的函数,不能有函数重载(符号表中的名字不会被修饰,无法形成函数重载)
引用
引用的概念
引用不是定义一个新的变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型&引用变量名 = 引用实体
void TestRef()
{
int a = 10;
int& ra = a;
//打印两个变量的内容
cout << a << endl;
cout << ra << endl;
//打印两个变量的地址
cout << &a << endl;
cout << &ra << endl;
}
引用的特性
-
引用在定义时必须初始化
-
一个变量可以有多个引用
-
引用一旦引用一个实体,再不能引用其他实体
①执行ra = x;之前
②执行ra = x;之后
常引用
void TestRef()
{
const int a = 10;
//ra引用a属于权限的放大,将一个原本只读的变量改为可读可写,所以编译器报错
//int& ra = a;
const int& ra = a;//ok!
int b = 100;
int& rb = b;
const int&rrb = b;//rrb引用b属于权限的缩小,因此可以通过编译
int c = 10;
double d = 3.14;
//将c赋值给d,发生隐式类型转换
//d = c;
//double&rc = c;//编译报错
const double& rc = c;
}
在该过程中会产生一个临时变量,c先将它的值存在该临时变量中,d取的是临时变量里面double类型的值,而非c自身。
为什么加上const后编译器就不会报错呢?
原因是 在该过程中也会产生一个double类型的临时变量,临时变量具有常性,因此加上const后就不会报错
注意:
- rc并不是c的别名,而是该过程产生的临时变量的别名
使用场景
做参数
void Swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
做返回值
int& Count()
{
static int n = 0;
n++;
return n;
}
结果如下图
那么,下面的代码输出结果是什么,为甚?
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;
}
输出结果如下图
原因如下
注
在函数返回时,出了函数作用域,如果返回对象未归还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回
传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回值期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
- 值和引用作为参数的效率比较
#include<time.h>
class 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 < 1000; i++)
{
TestFunc1(a);
}
size_t end1 = clock();
//以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 1000; i++)
{
TestFunc2(a);
}
size_t end2 = clock();
cout << "TestFunc1(A)-time is " << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time is " << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
- 值和引用作为返回值类型的效率比较
#include<time.h>
class A
{
int a[10000];
};
A a;
A TestFunc1()
{
return a;
}
A& TestFunc2()
{
return a;
}
void TestRefAndValue()
{
//以值作为函数返回值
size_t begin1 = clock();
for (size_t i = 0; i < 100000; i++)
{
TestFunc1();
}
size_t end1 = clock();
//以引用作为函数返回值
size_t begin2 = clock();
for (size_t i = 0; i < 100000; i++)
{
TestFunc2();
}
size_t end2 = clock();
cout << "TestFunc1(A)-time is " << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time is " << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
由上述结果可以看出,传值和引用在作为传参以及返回值类型上效率相差很大
引用和指针的区别
在语法概念上,引用就是一个别名,没有独立空间,和其引用实体共用同一块空间
int main()
{
int a = 10;
int& ra = a;
cout << "&a = " << &a << endl;
cout << "&ra = " << &ra << endl;
return 0;
}
在底层实现上其实是有空间的,因为引用是按照指针方式来实现的
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
下面我们来对比一下引用和指针的汇编代码
引用和指针的不同点
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用了一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但是有NULL指针
- 在sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下4字节,64位平台下8字节)
- 引用自增表示对引用的实体+1,指针自增表示指针向后偏移一个类型的大小
- 有多级指针,但没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对较为安全
内联函数
概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序的运行效率
如果在上述函数前加上inline关键字将其改为内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add
- 在debug模式下,需要对编译器进行设置,否则不会展开
因为debug模式下,编译器默认不会对代码进行优化,下面给出VS2013的设置方式
设置完毕后,我们对Add函数前面加上inline关键字,查看编译后的汇编代码
特性
- inline是一种以空间换时间的做法,省去调用函数的额外开销。所以代码段很长或者有循环,递归的函数不适宜做内联函数
- inline对于编译器只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环递归等,编译器优化时会忽略掉内联
- inline不建议定义和声明分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
面试题
宏的优缺点
优点
- 增强代码的复用性
- 提高性能
缺点
- 不方便调试宏,因为预处理阶段进行了宏替换
- 导致代码可读性差,可维护性差,容易误用
- 没有类型安全的检查
C++有哪些技术可以替代宏
- 常量定义 换用const
- 函数定义 换用内联函数
auto关键字(C++11)
auto简介
在早期C\C++中auto的含义是:使用auto修饰的变量,是具有自动存储的局部变量
C++11中,标准委员会赋予了auto全新的含义: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;
return 0;
}
【注】
使用auto定义变量的时候必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种类型的声明,而是一个类型声明时的“占位符”,编译器在编译阶段会将auto替换为变量实际的类型
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 = 40;
c = 60;
return 0;
}
- 在同一行定义多个变量
在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0;//报错,前后类型不一样
}
auto不能推导的场景
- auto不能作为函数的参数
//此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{
}
- auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = { 2, 3, 4 };
}
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟C++11提供的新式for循环,还有lambda表达式等进行配合使用
基于范围的for循环(C++11)
范围for的语法
在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 << ' ';
}
cout << endl;
}
范围for的使用条件
for循环的迭代范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围。对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围
以下代码就有问题,因为for的范围不确定
void TestFor(int array[])
{
for(auto& e:array)
{
cout<< e << endl;
}
}
空指针nullptr(C++11)
C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
- 在C++11中,sizeof(nullptr) 与 sizeof((void)0)所占的字节数相同。
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。