目录
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/23a79964073067a53f028553f7d6f093.jpeg#pic_center)
1.C++的起源
对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
2.C++关键字
c++有63个关键字,比起C语言的32个关键字,约多出一倍。
3.C++ 命名空间
函数和部分变量存在于全局作用域中,应避免函数名以及变量名在日后项目逐渐的扩大的过程中造成的重名冲突。
使用命名空间的目的就是对标识符的名称进行本地化,在自己命名的空间中声明,定义函数和变量以防止重名。
3.1 命名空间的定义
a.普通使用
namespace Mine //关键字——namespace 空间名——Mine
{
//定义变量
int time;
//定义函数
void Func()
{
printf("namespace Mine\n");
}
//定义结构体类型
struct Node
{
struct Node* next;
int data;
};
}
b.命名空间可以嵌套
namespace Mine
{
//定义变量
int time = 0;
//定义函数
void Func()
{
printf("namespace Mine\n");
}
//定义类型
struct Node
{
struct Node* next;
int data;
};
namespace Mine2
{
void Func()//注意这里函数的名字是Mine2专属的,即使嵌套在Mine的空间中,也不会造成冲突
{
printf("namespace Mine2\n");
}
}
}
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
c.命名空间的名字冲突了怎么办?
多个相同名称的命名空间会在编译时合并成一个命名空间,但是同命名空间下的同名变量或是函数名仍会造成冲突
!
//在source.h文件中
namespace myspace
{
int rand;
double Add(double a, double b);
int Mul(int a, int b);
}
//在source.c文件中
namespace myspace
{
//int rand;//这里会发生冲突,应避免
double Add(double a, double b)//可以函数重载
{
return a + b;
}
int Mul(int a, int b)
{
return a * b;
}
}
同名namespace,多用于头文件中的声明,和源文件中的定义。
3.2 命名空间的使用
- 方式一:添加命名空间名称和作用域限定符(
::
)
int main()
{
Mine::time = 10;
printf("%d\n", Mine::time);
Mine::Func();
Mine::Mine2::Func();
return 0;
}
- 方式二——使用using 释放一部分命名空间中成员可直接使用,但与此同时被释放的成员
失去了隔离
,将会有可能与全局作用域的某些量发生冲突!
using Mine::Mine2::Func;//释放了Mine2中的Func,现可以直接使用
int main()
{
Func();
return 0;
}
如果同时释放了Mine的Func
using Mine::Func;
using Mine::Mine2::Func;
int main()
{
Func();
return 0;
}
产生了冲突
关于函数重载,会在下文谈及。
- 方式3 ——使用using namespace +空间名,将命名空间的名称全数引入全局作用域
using namespace Mine;
int main()
{
Func();
Mine2::Func();
return 0;
}
4.C++ 输入和输出
C++的第一声呼唤
#include <iostream>
using namespace std;
int main()
{
cout<<"Hello world!"<<" "<<"from CPP"<<endl;
return 0;
}
使用cout
标准输出(控制台)和cin
标准输入(键盘)时,必须包含< iostream >头文件以及std标准命名空间。
int main()
{
int a;
double b;
cin>>a>>b;
cout<<a<<" "<<b<<endl;
}
- 符号>>和<<表示将数据输入和输出到流
- endl(end line)表示换行。
- 使用C++输入输出更方便,不需增加数据格式控制,比如:浮点型–%f,字符串–%s。
5.缺省参数
5.1 了解缺省参数
在声明或定义函数时
,为函数的参数设定默认值,在调用函数时,如果没有指定实参(缺省实参),则采用默认值作为参数。
void Func(int a=0)
{
cout<<a<<endl;
}
int main()
{
Func();
Func(10);
return 0;
}
5.2 缺省参数分类
- 全缺省参数
void Func1(int a = 1, int b = 2, int c = 3)
{
cout <<"a="<< a << " ";
cout <<"b="<< b << " ";
cout <<"c="<< c << endl<<endl;
}
实参从左到右依次给到形参
- 半缺省参数
缺省部分参数 – 必须从右往左缺省,且必须连续缺省
void Func2(int a,int b=1,int c=2)
{
cout <<"a="<< a << " ";
cout <<"b="<< b << " ";
cout <<"c="<< c << endl<<endl;
}
注意缺省参数不能在函数声明和定义中同时出现
//source.h
void TestFunc(int* a,int n=10 );
//source.c
void TestFunc(int *a,int n=10)
{}
如果定义和声明同时出现缺省,恰巧提供的默认值不同,会给编译器造成歧义。
- 缺省值必须是常量或全局变量
6.函数重载
6.1 函数重载概念
如果在同一个作用域的几个函数名字相同但参数列表不同(参数个数,顺序,类型不同),我们称之为重载函数。
参考下面的函数声明
void print(const char *cp);
void print(const int* beg,const int* end);
void print(const int ia[],size_t size);
这些函数接受的形参不同,编译器会根据实际接受的实参类型推断想要的是哪一个函数。
int a[2]={0,1};
int b[2]=[8,9];
print("hello world");//调用void print(const char *cp);
print(a,2);//调用void print(const int ia[],size_t size);
print(a,b);//调用void print(const int* beg,const int* end);
注意 main函数不能重载
- 函数重载与缺省参数
void f()
{
cout<<"f()"<<endl;
}
void f(int a=0)
{
cout<<"f(int a)"<<endl;
}
注意上面的两个函数是构成重载的,因为参数不同。但是使用时会造成歧义。
传参时,编译器可以区别出两个函数,但是不传参会报错。
6.2 函数重载原理(选读)
在一个程序运行起来要经过几个阶段:
预处理、编译、汇编、链接
我们使用linux的gcc编译器对如下程序进行编译,以编译程序test.c为例:
gcc -E
——预处理,生成的文件test.i
gcc -S
——编译生成汇编代码,生成的文件为test.S
gcc -c
——汇编生成机器码,生成的文件test.o
gcc
——执行链接,生成默认名为a.out的可执行文件
图示:
1.我们的多个项目会有多个头文件和源文件组成,在预处理阶段,头文件在包含的源文件中展开,源文件在各自编译时,只能获取到函数的声明【test.o调用了Add函数,但没有Add的函数定义】,而真正的Add函数在sum.o中定义。
2.链接器看到test.o调用了Add,但是Add没有地址,于是就到sum.o的符号表
中找到Add的地址,然后链接到一起。
3.那符号表是如何描述函数的呢?
我们在用gcc(C语言编译器
)编译时看到的结果如下:
很显然,c语言编译时,其符号表是直接记录函数名
的,同函数名即使参数不同编译器也不会去识别
,所以函数重载自然不能在C中使用
,
我们再使用g++(C++编译器)试一下:
g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】
,函数的名字修饰发生改变,函数参数类型信息添加到了修改的名字之后,即使是同名函数,也能通过参数的相异来判断使用的是哪一个,于是我们也就理解了函数重载要求参数不同,而与返回值没有关系
!
7. 引用(左值引用)
7.1 引用概念
引用为对象起了另外一个名字,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块内存空间。
类型& 引用变量名=引用实体;
int val=10;
int& refval=val;//refval指向val,是val的另一个名字
int& refval2;//错误,引用必须初始化
一般在初始化变量时,初始值会被拷贝到变量中,然而定义引用时,程序就把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用,一旦初始化完成,引用就和初始值一直绑定在一起。
为引用赋值,实则把值赋给了与其绑定的对象。
无法将引用重新绑定到另一个对象上,所以引用必须初始化
引用并非对象,它只是已经存在的对象的别名
以引用作为初始值,实际就是将绑定的对象作为初始值。
int& refval3=refval;//正确,这里就是绑定在val上
int i=refval;//正确,i被初始化为val的值
&
就是引用的标识符,在一行里定义多个引用,每个名字都需要以&开头
int i1=10,i2=20;
int &r1=i1,&r2=i2;
7.2 引用使用场景
引用的价值体现在函数调用中的传参和传值返回。
这两者都存在拷贝的情况:
1.做参数
引用在做函数形参时与指针价值相同
a.如果实参的数组较大,传值将拷贝全部数组,而
传指针
和传引用
可以节省了大量拷贝的时间:
struct A
{
int arr[100000];
};
void func1(struct A a)//传值
{}
void func2(struct A* a)//传址
{}
void func3(struct A& a)//传引用
{}
int main()
{
A a;
int begin1 = clock();
for (int i = 0; i < 1000; i++)
{
func1(a);
}
int end1 = clock();
int begin2 = clock();
for (int i = 0; i < 1000; i++)
{
func2(&a);
}
int end2 = clock();
int begin3 = clock();
for (int i = 0; i < 1000; i++)
{
func3(a);
}
int end3 = clock();
printf("传值耗时:%d\n", end1-begin1);
printf("传指针耗时:%d\n", end2 - begin2);
printf("传引用耗时:%d\n", end3 - begin3);
}
b.形参的修改可以影响实参(输出型参数)
2.做返回值
a.返回局部变量(错误)
由于函数作用域的变量
的返回是拷贝,千万不要返回函数作用域中局部变量的引用!因为在函数结束时栈帧销毁,无法保存引用的对象。
此时,虽可以得到函数局部变量的引用,但已引起非法访问,因为函数栈帧已名存实亡了。
在后续使用其他函数时,会对原有栈帧形成覆盖,引用的对象在不被保护的情况下,自然就被“掩埋”。
b.
出了作用域还存在的变量
可以使用传引用返回,如全局变量
,静态变量
,外部传进函数的参数变量
,malloc的空间对象
等。
返回引用的好处在于可读可写
,因为一旦返回引用就具有了左值的功能(可被修改)
:
const int N = 10;
int& At(int i)
{
static int a[N];
return a[i];
}
int main()
{
for (int i = 0; i < N; i++)
{
At(i) = N + i;//可对返回值赋值
}
for (int i = 0; i < N; i++)
{
cout << At(i) << endl;
}
}
传引用的函数重载
传引用可以重载,但调用时与传值产生歧义,不知道调用时是传值还是传引用。
1.引用在传参和传返回值,在有些场景中,可以提高性能(大对象+深拷贝对象)
2.引用在传参和传返回值,输出型参数和输出型返回值。通俗点说,形参的改变可以改变实参。可以改变返回对象。
7.3常引用
- const Type& 引用名
将引用绑定在const对象上,我们称之为对常量的引用。
常量引用绑定非常量对象,字面值,以及一般表达式
见如下报错:
当添加const后,报错消失
要想理解这种例外情况的产生,最简单的办法就是弄清楚当一个常量引用被绑定到另一个类型上时,到底发生了什么:
double a=1.1;
const int& ra=a;
ra为int型引用,但是却绑定在了一个双精度浮点数上,因此为了确保ra绑定的是一个整型,编译器进行了如下修改:
const int temp=a;
const int& ra=temp;
这里ra绑定了一个带有常性的临时量对象,所谓的临时量对象就是编译器需要一个空间来暂存表达式的求值结果临时创建的未命名变量
。
临时变量是右值,其所接受的隐式类型转换,表达式结果,字面值
都具有常量属性
无法修改,所以需要加const。
- 如果函数不修改参数的值,使用常量引用作为形参,既可以节省拷贝的时间空间,也能够防止函数对形参进行修改
void StackPrint(const struct Stack& st);
传来的形参如果是const对象,普通对象,临时对象
,const引用作为形参都是通吃
的。
7.4 指针和引用的区别
- 引用在定义时必须初始化,指针并不是
- 引用在初始化时绑定一个实体后,就不能再引用其他实体。而指针没有这个限制
- 没有NULL引用,但可以定义NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 运算:引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
8. 内联函数
以 inline
修饰的函数称为内联函数,C++编译器在编译时会在调用内联函数的地方将函数代码块展开(这点很像C语言中的宏替换(预处理)
),由于没有函数栈帧的开销,内联函数可以提高程序运行时的效率。
调用函数一般比求等价的表达式慢一些,其中包括:调用前保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置执行。
8.1 回顾 宏
与函数相比,define定义的宏只是在预处理阶段对文本进行了替换,随后交给编译器去编译,故宏不具备类型检查,而且在使用中常出现意想不到的错误,最容易犯得错误就是忘记添加圆括号
#define Square(x) x*x
我们想通过这个宏定义得到两数之和,但是当我们如此调用它时,则会出现差错:
int answer=Square(2+3);
初衷是计算(2+3)的平方,但是宏替换后变成 2+3*2+3,源代码的语义就发生了改变。
于是我们就添加上了小括号,保证了参数是完整的,不会因为优先级的问题而改变代码的本意。
#define Square(x) (x)*(x)
你以为这样就没问题了吗?考虑 Square(a++)
被宏替换后是(a++)*(a++),a是否被计算了两遍,将由编译器说了算。于是就造成了使用宏的困惑。
尽管如此,宏在于其直接展开的高效性,减少了大量调用函数时所带来的栈帧的开销,尤其是当函数体本身较为短小且大量调用的情况下,宏替换的价值体现的淋漓尽致。直接在调用处展开,较少了频繁调用函数的开销提升程序的效率。
那我们如何继承宏的优良性能,同时避免宏的缺点呢——内联函数。
8.2 inline 内联函数
在包含宏直接展开且无需函数调用的基础上,内联函数还具备先计算参数,类型检查,作用域限制等普通函数就具备的特性。
无内联时是直接调用函数
如果在上述函数前添加inline关键字,将其改为内联函数
查看方式:
- 在release模式下查看汇编代码中是否存在call Add
- 在debug模式下,先对编译器进行设置,否则不会展开,因为debug模式下,编译器不进行优化。
8.3 特性
-
inline是用空间换时间的做法,代码很长,循环/递归的函数不适合使用内联函数:
内联函数展开的原理,是在调用处将整个函数体展开(栈帧操作、返回等忽略了),所以如果某个函数被反复调用,而函数体本身较长,那么目标码的体积就会急剧膨胀,减少函数调用开销带来的性能提升很可能被内存紧张而抵消掉,这会严重降低性能。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,所以也没有必要用内联函数实现。
-
inline对于编译器只是建议:
具体是否产生了内联这一行为,inline这一关键字是并不能起到决定性作用的。在现在的编译器下,内联往往成为了一种编译器行为,编译器会根据具体的情况做出适当的取舍。人为地使用inline关键字,只是给了编译器一条建议:最好能把这个函数内联了。既然是建议,那就有好有坏,未必必须要遵从执行;编译器既然有权采纳你的建议,当然也有权拒绝了。
-
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到,所以内联函数不是不能分离定义,而是在调用点内联函数的定义必须可见:
C/C++编译器在编译时,是在预处理器展开#include指令包含的头文件后逐个编译源代码文件。在内联函数没有被包含却被跨文件调用时,某些编译器很有可能会出现“无法解析的外部符号”这一链接错误。
-
调试面对内联函数束手无策:
对于内联函数,当它被展开嵌入进主调函数时,编译器是无法跟踪其运行的,因此往往会出现一种“设置了某个断点,却无法命中”的情况。而对于编译器偷偷摸摸擅作主张的内联,就要留个心眼了。最起码,在碰到问题而断点不命中时,在心里得有这个意识:是不是编译器在后面做鬼内联,让我的断点失效了?这时候就得试着关闭编译器的内联选项,再观察断点和进一步调试。虽然导致断点失效的情况可能很多,但是内联函数确实一个很重要的原因。
这里感谢博客指导:内联函数那些事情
9.auto关键字
9.1 auto简介
编程时常常需要把表达式的值赋给变量,这就需要在声明变量时清楚的知道表达式的类型。然而有时并不容易做到这一点。为了解决这个问题,C++11引入了auto类型说明符,用它就能让编译器帮我们去分析表达式的所属类型,auto让编译器通过初始值来推断变量的类型,所以auto定义的变量必须有初始值:
auto val=i+j;//由i+j的结果推断出val的类型
- auto使用案例
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;//typeid(变量名).name() ——返回该变量的类型
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
注意:auto并非类型,而是类型声明时的“占位符”,在编译时,编译器会将auto替换成变量实际的类型。
9.2 auto的使用技巧
- auto与指针和引用的结合
用auto声明指针类型时,使用auto和auto* 得到的变量类型是一致的
当引用作为初始值时,真正参与初始化的是引用的对象的值,所以auto声明引用类型必须加 &
:
2.在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对
第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
9.3 auto 不能推导的场景
- auto不能作为函数的参数
- auto不能用来声明数组
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等
进行配合使用。
10.基于范围的for循环(C++11)
10.1 范围for的语法
如果使用传统的for循环去遍历数组:
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循环的括号中有两个变量由冒号隔开,前者为迭代的变量,后者为被迭代的范围:
for (declaration : expression)
statement
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
}
结果:
与传统循环类似,可以用continue结束本次循环,也可以使用break跳出循环。
10.2 范围for的使用条件
-
for迭代的范围必须是确定的
对于数组而言,数组的第一个元素和最后一个元素的范围,对于类而言,应该提供begin和end的方法
以下代码无法确定数组范围:void TestFor(int array[]) { for(auto& e : array) cout<< e <<endl; }
11.指针空值nullptr(C++11)
11.1 C++98的指针空值
声明指针时常会赋值一个空指针——NULL,但NULL实际上是一个宏,我们在C的头文件(stddef.h)中找到了定义NULL宏的代码:
可以看到NULL在C++文件中被定义为字面值0,只有在C文件中会被定义为无类型指针的常量,在使用空值指针时会遇到一些麻烦:
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
函数是构成函数重载的,我们调用的目标也很明确,f(0)
调用函数 void f(int)
,f(NULL)
调用函数 void f(int*)
,但是由于NULL在C++中被定义为字面值0,除非对其强转,故
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
青山不改,绿水长流