1.C++的由来
C++由Bjarne Stroustrup(本贾尼·斯特劳斯特卢普)于20世纪80年代研发并实现,它是基于C语言,由C语言改良而来,继承了C语言的面向过程设计也支持面向对象设计,其功能十分强大。
2.推开C++的大门——输出Hello World
C++兼容C语言绝大部分语法,比如使用C++编译器编译一个C语言程序。
进入正题,使用C++语法输出Hello World:
#include <iostream>//与C语言一样,包含头文件,这个头文件是输入输出流
using namespace std;//这个叫展开命名空间,不知道也没关系,后续会讲解
int main()
{
cout << "Hello World" << endl;//这个类比C语言的printf,输出xxx,endl是换行符
return 0;
}
3.C++的关键词(粗略介绍)
asm | do | if | return | try | continue |
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw |
case | enum | mutable | static | union | wchar_t |
catch | explicit | namesapce | static_cast | unsigned | default |
char | export | new | struct | using | friend |
calss | extern | operator | switch | virtual | register |
const | false | private | template | void | true |
const_cast | float | protected | this | volatile | while |
delect | goto | reinterpret_cast |
total
63个 |
4.命名空间
为了解决C语言的命名冲突问题,C++提出了namespace来解决。例如以下是一段C语言程序的命名冲突问题。
但是rand作为局部变量是可以编译运行的,因为stdlib库里有rand()函数的实现,当预处理阶段展开头文件的时候,再来一个全局变量rand就会产生命名冲突,而在C语言中我们知道局部变量名称可以和函数名一样(不在同一个域中)。
我猜是这样的:定义在全局的Add会被优先识别为函数;而定义在main函数中的Add,会被优先识别为变量,因为此时Add的作用域在main函数中,编译器会优先选择Add作为变量,而不是函数。 关于变量的作用域与生命周期可以看这篇文章的第6点:写文章-CSDN创作中心,而更多的时候发生命名冲突问题是由于程序员定义的东西跟库里定义的东西发生了命名冲突,往往是xx重定义。
4.1命名空间定义
先补充一个关于域的知识点:
全局变量x的作用域是全局的,所以它对应的是全局域,而局部变量x的作用域是局部的,所以它对应的是局部域,很明显编译器会优先访问局部域变量x,那如果我们想访问全局域x呢?这时就需要用到域作用限定符::
::x,左边没有给定就是默认全局域。
一般常见的域有:全局域,局部域,命名空间域,类域 。
命名空间的定义需要用到关键字namespace,后面跟名字,然后再跟一对{}即可,{}中的就是命名空间成员。
#include <iostream>
namespace world1
{
int x = 5;
}
namespace world2
{
int x = 3;
}
int main()
{
printf("%d\n", world1::x);
printf("%d\n", world2::x);
return 0;
}
补充:命名空间不会影响它的生命周期,只会影响访问! 所以定义在全局的那两个x任然是全局变量,作用域是全局,用域作用限定符即可访问某一域中的成员。编译器的搜索原则是1.当前局部域,2.全局域,3.若指定了域,则直接去指定的域中搜索。
命名空间还可以定义函数,结构体,命名空间
#include <iostream>
namespace world1
{
int x = 5;
int Add(int x, int y)
{
return x + y;
}
struct MyStruct
{
int x;
int y;
};
namespace m
{
int z = 0;
}
}
namespace world2
{
int x = 3;
}
int main()
{
printf("%d\n", world1::x);
printf("%d\n", world2::x);
printf("%d\n", world1::Add(1, 2));
struct MyStruct* phead1;//这里比较怪,可以不指定命名空间
struct world1::MyStruct phead2;//注意这里也比较特殊
world1::m::z;
return 0;
}
补充:同一个工程可以存在多个相同的命名空间,编译器会合成到同一个命名空间中。
4.2命名空间的使用
1.加命名空间名称及作用域限定符
2.使用using将命名空间中某个成员引入
3.使用using namespace 命名空间名称 引入(展开命名空间)
所以回到输出Hello World
cout,endl被封装在std这个命名空间中,所以要使用,可以使用以上三种方法。
补充:std是所有c++库命名空间,展开命名空间不能改变它的访问优先级,即访问原则任然是先局部再全局,展开只是放归全局。平常做练习可以using namespace std;可以图方便,但是在项目中是不能随意展开的(也可以使用using将命名空间的某个常用成员展开)!容易造成命名冲突!
5.C++输入和输出
C++的头文件大多不带.h,.h是后缀,用以区分是C的头文件还是C++的头文件(但古老的C++编译器仍需要加.h后缀,比如vc6.0)
cin是标准输入流对象(键盘),>>在C++中有两种意思,一种是右移运算符,二是流提取运算符,我这里在键盘上输入2,那 i 就等于2了。<< 在C++中有两种意思,一种是左移运算符,另一种就是流插入运算符,cout是标准输出流对象(控制台),endl是换行符,他们都包含在<iostream>头文件中,可以看出使用C++输入输出更方便,不需要像printf/scanf那样手动控制格式输入输出,他能自动识别变量类型。
关于printf格式化打印和cout,一般情况下使用cout,但是要控制输出精度的时候,往往printf更简单,eg:
关于为什么使用了printf却没有包头文件<stdio.h>是因为在windows平台下,iostream这个头文件有这个函数的实现,而相比Linux平台下,iostream这个头文件则没有printf的实现(或者间接包含了stdio.h这个头文件),需要包stdio.h这个头文件。
6.缺省参数
6.1缺省参数的概念
先看一个C语言例子:
再来看C++可以怎么玩->
6.2缺省参数的分类
a.全缺省参数
#include <iostream>
using namespace std;
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func(1, 2, 3);
Func(1, 2);
Func(1);
Func();
return 0;
}
不能跳跃传参,实参必须是从左往右依次传参,Func(, 1, 2)是错误的。
b.半缺省
注意:1.半缺省参数必须从右往左连续给,不能跳跃着给;2.缺省参数不能在函数声明和定义同时给;3.缺省值必须是常量或者全局变量。
缺省参数在声明和定义分离时的规范:必须在声明的地方给,因为包的是.h文件,.h文件会被展开(在预编译阶段),展开的内容只有函数的声明,所以应该在声明的地方给缺省参数,C++语法不支持在声明与定义分离时在定义的地方给缺省参数。
7.函数重载
7.1概念
C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。有点像“一词多义”。
a.参数个数一样但类型不一样
#include <iostream>
using namespace std;
int Add(int x, int y)
{
cout << "int Add(...)" << endl;
return x + y;
}
double Add(double x, double y)
{
cout << "double Add(..)" << endl;
return x + y;
}
int main()
{
int a = Add(1, 2);
double b = Add(3.3, 1.1);
cout << a << " " << b;
return 0;
}
b.参数个数一样但类型顺序不一样(本质还是类型不一样)
#include <iostream>
using namespace std;
void fuc(int x, char y)
{
cout << "fuc(...)" << endl;
}
void fuc(char x, int y)
{
cout << "fun(..)" << endl;
}
int main()
{
fuc(1, 'a');
fuc('b', 2);
return 0;
}
c. 参数个数不一样
#include <iostream>
using namespace std;
void fuc(int x)
{
cout << "fuc(...)" << endl;
}
void fuc(int x, int y)
{
cout << "fun(..)" << endl;
}
int main()
{
fuc(1);
fuc(1, 2);
return 0;
}
!!!C语言不支持函数重载那CPP是如何支持的呢?
在C语言中我们知道在一个工程中,往往要做声明与定义分离,那么此时,在main.c文件中往往是包含其他方法(函数)声明的.h文件,那么调用方法(函数)的时候,在汇编层面是call 函数名(???)eg:
这里要说明一下,在main.c文件中,只包含了函数的声明,单单对于这个.c文件在经过编译还没有链接时,是生成不了函数的地址,因为函数的实现在fuc.c文件中,所以在汇编的角度是call fuc(???),只有在链接的时候,通过符号表,才能找到函数的真正地址!
所以当按照C语言调用函数的逻辑就会出问题:对于fuc(1, 'a') //call fuc(?), 对于fuc('a', 1) //call fuc(?),在生成这两个函数的符号表里有两个fuc的函数,那链接的时候谁是谁呢?在main函数直接有函数的定义的时候还好说,但是在声明定义分离的时候,就会傻傻分不清了,那怎么办呢?(这也就是C语言不支持函数重载的原因)所以,C语言在链接时,是直接用函数名去找地址,所以不允许出现同名函数。但C++是不单单是用函数名来这样操作了,它还带了参数,=》函数名修饰规则:名字中引入参数的类型,各个编译器自己实现。在Linux下fuc(1, 'a') //call _Z3fucic(?);fuc('a', 1) //call _Z3fucci(?),_Z我也不知道是啥意思,3是fuc这三个字符占3字节,i和c就是int和char的缩写,如果没有3这种函数名占几个字节也容易出bug,因为只根据函数名+类型->fucci可能是fu(char, char, int)也可能是fuc(char, int),这里要确定它的唯一性,所以要有函数长度字节数,而且函数名长度数不一样的一下就能区分开,加快了匹配速度。VS(Windows)下函数名修饰规则相对复杂一点。
8.引用
8.1引用概念
引用不是重新定义一个变量,而是给已经存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
长这样:类型& 引用变量名(对象名)= 引用实体。(当前阶段,变量和对象可以近似理解为一个东西,C++喜欢叫对象,C语言喜欢叫变量)
#include <iostream>
using namespace std;
int main()
{
int a = 6;
int& b = a;
cout << b << endl;
cout << &a << endl;
cout << &b << endl;
return 0;
}
=》b和a的地址一样,说明a和b都是同一块内存空间,且b是a的别名,二者都是指同一块空间。(变量名只是为了让程序员好记忆操作,实际上操作的是内存空间!)
#include <iostream>
using namespace std;
int main()
{
int a = 6;
int& b = a;
cout << b << endl;
cout << &a << endl;
cout << &b << endl;
b++;
a++;
int& c = a;
return 0;
}
=》对a,b的操作都是对a的操作,一个对象可以有多个别名。甚至还可以对别名取别名eg:int& d = c;也是ok的。
8.2使用场景
8.2.1做参数
#include <iostream>
using namespace std;
void Swap(int* a, int* b)
{
//...
}
void Swap(int& a, int& b)//引用接收
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 1, y = 0;
Swap(&x, &y);//传指针
Swap(x, y);//传值
return 0;
}
=》从语法上来说,a就是x的别名,二者是同一块空间,没有传东西。以前值拷贝的时候,形参会在函数栈帧中开一块空间,实参的值会copy给形参,二者对应的不是同一块空间,所以对形参的操作不会影响实参,而现在则不一样了。(语法上别和底层搞混了,语法规定是这样,人家祖师爷设计的),这里做个小规范,实参和形参的变量名可以一样,因为在不同域内,但是引用的时候,尽量二者名字不同。
=>当对象比较大(字节数)的时候,传对象会产生拷贝,会耗时,但引用则会减少拷贝,提高效率。eg:
#include <iostream>
using namespace std;
#include <time.h>
struct A
{
int a[10000];
};
void TestFunc1(A a)
{}
void TestFunc2(A& a)
{}
int main()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
return 0;
}
8.2.2做返回值
先来看这样一段代码:
int func()
{
int a = 1;
return a;
}
int main()
{
int ret = func();
return 0;
}
Q:func()函数的返回值是a吗?
A:不是a,出了func函数的作用域,a就销毁了(不同的编译器销毁的方法不一样,有些是置随机值,VS下a的值仍然是1),所以编译器的实现肯定不会让a去做返回值,而是在参数的传导过程中产生一个临时变量,会把a的值copy给临时变量,当func函数的栈帧销毁的时候,临时变量(C++规定临时变量具有常性)还在,再把临时变量的值赋值给ret,当临时变量比较小的时候,通常会用寄存器来充当。
如果是传引用返回又会如何呢?
int& func()
{
int a = 1;
return a;
}
int main()
{
int ret = func();
return 0;
}
=》这次返回的是a的别名,但a已经随着函数栈帧的销毁已经销毁了,所以会造成非法访问,有点像野指针的“野引用”。ret的值是不确定的,要看编译器怎么实现这块的。
再来看几个类似的
int& func()
{
int a = 1;
return a;
}
int main()
{
int& ret = func();
return 0;
}
这里ret也就会变成a的别名,同一块空间。
#include <iostream>
using namespace std;
int& func()
{
int a = 1;
return a;
}
void fx()
{
int b = 0;
}
int main()
{
int& ret = func();
cout << ret << endl;
fx();
cout << ret << endl;
return 0;
}
!!!用的是x86,要是用x64那打印出来的是0。
#include <iostream>
using namespace std;
int& func()
{
int a = 1;
return a;
}
int& fx()
{
int b = 0;
return b;
}
int main()
{
int& ret = func();
cout << ret << endl;
fx();
cout << ret << endl;
return 0;
}
要是func的返回值没有加&则又会返回随机值。
=》在x86环境下,当两个函数栈帧大小一样时(func和fx对称),内存空间复用了,当func函数栈帧销毁时,原先存a那块空间的值没有被清,然后fx函数中b的空间地址和a的一样,然后对b修改(也就是对那块空间修改),而ret是a的别名,也是那块空间,所以第二次打印出ret的值是0。所以,返回变量出了函数作用域,生命周期就没了(局部变量),不能用引用返回。
以下是一些可以使用引用返回的例子:
int& func()
{
static int a = 1;
return a;
}
int main()
{
int& ret = func();
return 0;
}
a不在func函数的栈帧中,在静态区,所以可以用引用返回。常见的可以用引用返回的有:全局变量、静态变量、堆上的变量等等。
补充一个点:C++规定临时变量具有常性
#include <stdlib.h>
int func()
{
int* a = (int*)malloc(sizeof(int) * 4);
return a[0];
}
int main()
{
func() *= 2;
return 0;
}
=>在不使用引用返回的情况下,会产生临时变量,临时变量可以理解为用const修饰的变量, 所以会出现当前报错信息;而引用返回的情况下则不会产生临时变量eg:
=》 引用返回具有读写返回变量的功能(前提是变量没有被销毁),修改了返回对象(指不使用引用返回,返回的是个copy变量),减少拷贝提高了效率。
8.3引用特性:
8.3.1引用在定义时必须初始化
eg:int a = 10; int& b; b = a,这种写法是错误的!
8.3.2一个变量可以有多个引用
eg:int a = 10; int& b = a; int&c = a(int& c = b);是ok的。
8.3.3引用一旦引用一个实体,再不能引用其他实体
引用定义后不能改变指向,eg:int a = 10; int& b = a; int c = 2; b = c;这段代码的意思是把c的值赋值给b,不是让b变成c的别名(引用必须要专一)。
Q:既然引用和指针的作用是类似的,那是否意味着指针就可以退休了?让引用来接替它的工作?
A:C++的引用,对指针使用比较复杂的场景进行一些替换,让代码更加简单易懂,但是不能完全替代指针。因为引用的第三大特性:引用定义后,不能改变指向。eg:在定义链表,二叉树的时候——你敢把前驱指针写成“前驱引用”?struct Node* prev=》struct Node& prev?那在插入删除的时候咋搞呢?注:C++的引用和Java、python的引用有点差别,这些语言没有指针,用的是引用,且链表有的引用实现。
8.4引用和指针的对比
1.引用是别名,不开空间;指针是地址,需要开空间存地址(我的理解是,需要一个指针变量来记录地址)。但底层的实现上二者是一样的(只有这一点是底层的角度,其他的都是语法角度),eg:
int main()
{
int a = 5;
int& ra = a;
ra = 10;
int* pa = &a;
*pa = 10;
return 0;
}
2.引用必须初始化,但是指针可以不初始化,可以初始化。
3.引用不能改变指向,指针可以。
4.引用相对更安全,没有空引用,但是有空指针,容易出现野指针,但不容易出现野引用。
5.在sizeof中含义不同:引用结果为引用类型大小,但指针始终是地址空间所占字节数(32位平台下就是4字节)
6.引用自增即引用实体加1,指针自增即指针向后偏移一个类型的大小
7.有多级指针,但没有多级引用。
8.访问实体方式不同,指针需要显式解引用,引用则编译器自己处理。
9.引用比指针使用起来相对安全。
9.内联函数
内联函数的提出是为了解决一些频繁调用的函数,因为函数的建立和销毁是在栈帧的基础上的,是需要消耗资源的。C语言是利用宏函数来解决的,而C++则认为宏函数坑点太多(太复杂,不容易控制;还不能调试;没有类型安全的检查),所以用内联函数来替代。eg:
int Add(int a, int b)
{
return a + b;
}
#define Add(a, b) ((a) + (b))
//C++
inline int Add(int a, int b)
{
return a + b;
}
加了关键字inline还这样是因为编译器优化了,怕你不好调试,需要更改一下设置=》
右击项目选择属性=》
这是配置完成后的debug模式下,在release模式下也是不建立栈帧,直接展开的。
那是否意味着我们可以随意在函数前面加inline?no,大一点的函数就不适合内联了,因为这会导致代码膨胀——假设有一个fuc函数的指令有100行,有1w次调用的地方,那就是要跑1w*100行指令;但是不使用内联,只有100+1w个call指令,这就会导致编译好的可执行程序变大。不同的编译器对于inline的机制不一样,通常内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。一般来说,内联机制用于优化规模小、流程直接、频繁调用的函数,很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点用内联展开。我对于内联机制的理解是:它发生在编译阶段,展开的实际是内联函数逻辑的指令展开,而不是在预处理阶段的替换。
接下来插入一点关于这方面与链接的关系=》
当.h文件在不做声明与定义分离时,会发生链接错误——函数重定义,因为两个包含了.h文件的.c文件会展开头文件,在底层的视角看来就是两个.c文件编译的Add函数进符号表发生了冲突。 那要避免这种冲突可以选择1.声明与定义分离。2.加static修饰,表示只限于当前文件使用,切断了外部链接,只能内部链接。
3.加inline修饰,因为inline函数是不会有call指令产生,是用函数逻辑的指令替代。2和3从底层来讲都是没有进符号表,所以就不会发生冲突。
而且,最最最重要的一个点就是:内联函数不可以声明定义分离!因为它没进符号表,当声明定义分离时,头文件展开得到声明,调用的地方会call函数地址,但是内联函数的定义是不进符号表,没有外部链接属性,所以会报错。且报错信息是在链接时,找不到函数地址。
有一道面试题是关于宏的优缺点:
优点是:增强代码的复用性;提高性能。
缺点是:不方便调试;代码的可读性变差,可维护性变差,容易误用;没有类型安全的检查。
C++可以用const、enum来替换常量的定义;短小宏函数定义换成内联函数。
10.auto关键字(C++11)
auto关键字的作用是自动推导类型,必须初始化。eg:
int main()
{
int i = 0;
int j = 1;
auto k = i;
//auto m;这是不可以的
auto p1 = &i;
auto* p2 = &i;
//auto* p3 = i;这也是不可以的
auto& p4 = i;//这是引用+auto
return 0;
}
一般在以下场景下常用自动类型推导
#include <iostream>
using namespace std;
void fuc(int a, int b)
{
//
}
int main()
{
void (*pf1)(int, int) = fuc;//函数指针
auto pf2 = fuc;
cout << typeid(pf1).name() << endl;//打印类型
cout << typeid(pf2).name() << endl;
return 0;
}
当然C语言也可以typedef去定义typedef void (*pf) (int, int)。auto用在类型很长的地方很爽。
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
std::map<std::string, std::string> dict;
std::map<std::string, std::string>:: iterator it1 = dict.begin();
auto it2 = dict.begin();
cout << typeid(it1).name() << endl << typeid(it2).name() << endl;
return 0;
}
但是auto也有不好的一面,不熟悉的类型无脑用auto代码的可读性非常差。auto不能做函数参数,不能用来声明数组,但可以作为函数返回值(有点py的味道)。
auto+范围for(C++11)
#include <iostream>
using namespace std;
int main()
{
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;
for (auto e : array)//依次取数组中的元素赋值给e,自动迭代,自动判断结束,底层是迭代器
{
cout << e << ' ';
}
cout << endl;
return 0;
}
这是auto搭配范围for遍历数组,也可以用int e,因为数组是int类型。当想对数组中的元素*=2可以使用引用auto& e。
范围for循环迭代范围必须是确定的,对于数组而言,就是数组的第一个元素和最后一个元素;对于类而言,应该提供begin和end方法,begin和end就是for循环迭代的范围。而以下代码就有问题,因为for的范围不确定。
void TestFor(int array[])
{
for (auto& e : array)
cout << e << endl;
}
//C语言是一个极度关注效率的语言,传地址的效率远大于把数组的值copy一份传过去。
11.指针空值
C++98中的指针空值和C语言一样都是NULL,NULL实际上是一个宏,其值是0。C++NULL会出bug。
在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采用哪种定义 ,在使用空值指针的时候,都会不可避免遇到一些麻烦,比如:
#include <iostream>
using namespace std;
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;
}
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入
的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。