此篇,就正式进入C++的学习了。
C++,高级语言,从名字也能窥见一二,是C语言的 “升级版”。它对C语言中许多空白的地方进行填补,不足的地方进行优化,允许我们 “面向对象编程”。因此我们在学习过程中可以多多对比C++和C。它的优势也和C一样,可以直接操控硬件,相比其他高级语言较底层,因此适合做游戏开发、服务器开发等等偏后端的事儿。
今天是C++学习的第一站,我们先来学学基础语法第一课——C++入门
主要内容有:
C语言中关于命名,同个域中不允许变量、类型同名,也不允许函数同名。这会带来一些问题
#include
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
:10
现在可以正常输出,那我这样呢?
#include
#include
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
此处就是自己定义的变量和库里的冲突,说到这,我们再问一个问题:
#include
//#include
int rand = 10;
int main()
{
int rand = 20;
printf("%d\n", rand);
return 0;
}
:20
局部 ==> 全局 ==> 找不到则报错
C++为了解决这个问题,提出了命名空间的概念…
namespace 命名空间是C++内置的关键字
先看看它用起来是什么样的
#include
namespace bacon
{
int pig = 10;
}
int main()
{
printf("%d\n", bacon::pig);
}
:10
语法:namespace [命名空间名]
有点像结构体啊,是的,结构体和命名空间的本质都是封装,只不过结构体封装的是类型,命名空间封装的是“名字”,它的本质是:
隔离 全局变量、类型、函数
并改变编译器查找规则
改变编译器查找规则?代码里的 : : 是什么东西?
语法:mynamespace : : numbers
命名空间 : : 成员
上面的代码中, bacon : : pig 的含义就是,在命名空间bacon中查找pig成员,这也是“改变编译器查找规则”的意思
不指定命名空间:局部 ==> 全局 ==> 报错(不指定bacon,编译器查找的时候就会当作没看到bacon)
指定命名空间
在命名空间找
printf("%d\n", bacon :: pig);
在全局找
printf("%d\n", :: pig);
命名空间的使用分以下三种:
使用时指定成员
printf("%d", bacon :: pig);
:10
隔离效果最佳,但是使用较麻烦
引入指定成员
using bacon :: pig;
printf("%d", pig);
:10
常用的可以引入
引入整个命名空间
using namespace bacon;
printf("%d ", pig);
printf("%s", song);
:10 Mojito
隔离失效,平时练习可以这么用,做项目之类的就别了
namespace bacon
{
int pig = 10;
namespace idol
{
namespace jay
{
const char* song = "Mojito";
}
}
}
int main()
{
printf("%s\n", bacon::idol::jay::song);
}
:Mojito
了解完命名空间,我们进一步学习 hello world 程序
#include
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
:hello world
#include
using namespace std:std,C++标准库的命名空间名,标准库的定义实现都在这个命名空间中
*c++中没有定义输入输出的语句,而是包含了一个全面的标准库来提供IO机制。iostream库包含两个基础类型 istream 和 ostream 表示输入输出流。一个流就是一个字符序列,“流”想表达的是,随着时间推移,字符序列顺序生成或消耗
用printf方便就用呗!哪个方便用哪个
缺省参数,就是在声明函数的某个参数的时候,为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就无须指定该参数
缺省参数也分 全缺省 和 半缺省:
全缺省
#include
using namespace std;
int f1(int e1 = 10, int e2 = 20)
{
return e1 + e2;
}
int main()
{
int ret = f1();
cout << ret << endl;
return 0;
}
:30
半缺省:只能从右往左连续缺省(需要缺省的往后放)
int f2(int e1, int e2 = 20)
{
return e1 + e2;
}
int main()
{
int ret = f2(100);
cout << ret << endl;
return 0;
}
:120
注意:
缺省参数不能在函数的声明和定义中同时出现(我们放在声明里就好)
缺省参数一般是 常量 或 全局变量
函数重载(fuction overload),指 我们可以通过传不同的参数来区分同名的函数。
简单来说,“一词多义”,一个函数名能调用执行不同的功能;反过来说,不同功能的函数可以同名。只需要:参数列表不同(类型、个数、顺序)。简单归纳:
构成函数重载:
#include
using namespace std;
int Add(int e1, int e2)
{
return e1 + e2;
}
double Add(double e1, double e2)
{
return e1 + e2;
}
int main()
{
int i1 = 10;
int i2 = 20;
double d1 = 1.1;
double d2 = 2.2;
int reti = Add(i1, i2);
double retd = Add(d1, d2);
cout << reti << endl;
cout << retd << endl;
return 0;
}
:30
3.3
cin/cout 的自动识别类型也是函数重载,通过参数列表区分不同的输出类型
现有两个构成重载的函数,调用时我们传的参数无法让编译器明确区分两个函数,我们称这两个构成重载的函数有 二义性
#include
using namespace std;
void f()
{
cout << "f()" << endl;
}
void f(int e1 = 0, int e2 = 0)
{
cout << "f(int e1 = 0, int e2 = 0)" << endl;
}
int main()
{
return 0;
}
不调用并编译,能编译成功,说明构成重载
现在来调用一下看看
#include
using namespace std;
void f()
{
cout << "f()" << endl;
}
void f(int e1 = 0, int e2 = 0)
{
cout << "f(int e1 = 0, int e2 = 0)" << endl;
}
int main()
{
f();
return 0;
}
实际使用中要注意避免产生二义性
函数重载的原理不适合现阶段解剖,我们浅浅了解一下就够
我们知道,函数调用就是找到函数名对应的地址,执行地址处的函数体,但,相同的函数名怎么找到不同的地址?
虽然C++中构成函数重载的函数名看起来一样,但其实不尽相同…
我们先来回忆一下 依函数声明找函数定义 的过程,
程序从源文件到可执行程序:
源文件 ==预处理==> ==编译==> ==汇编==> ==链接==> 可执行程序
*段表:把逻辑段映射到物理内存区
*符号表:编译过程时,源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址
再来看:test.c 中调用了 Add,但编译器在编译后链接前,要找函数定义的时候,发现 test.o 的目标文件内没有函数地址(找不到函数定义),就到 Add.o(其他目标文件) 中找,找到后链接(找不到就是链接错误)
接下来再看C和C++中,找函数定义时的区别:
(由于win的vs下函数名修饰太复杂,Linux下的gcc/g++就很好理解,所以下面用gcc演示)
int Add(int a, int b)
{
return a + b;
}
void func(int a, double b, int* p)
{}
int main()
{
Add(1, 2);
func(1, 2, 0);
return 0;
}
可以看到,C编译器编译后,函数名不变;C++编译器编译后函数名被修饰了:[ _Z + 函数名长度 + 函数名 + 参数类型首字母 ]
我们使两个函数构成重载时,参数列表的不同,已经决定了两个函数是“不一样的函数”了,怎么说?
不同的参数列表,使函数名被修饰成不同的样子,有了修饰后 根本意义上不同的函数名,这样一来,C++编译器通过函数名修饰规则,就能找到对应代码
不能的话,那我们在函数名修饰中带上返回值类型不就好了吗?
:不能构成,而且也并不是因为函数名修饰,而是 调用时的二义性——
调用的地方不能控制返回值的类型,我调用返回值为int的,你给我调用返回值为char的,那可不行
引用,一种数据类型,像是变量的同位语,或是给它取别名,一块空间的不同名字
比如: 周杰伦,JayChou,很多人的青春…
周杰伦就是JayChou,JayChou就是很多人的青春,没有区别
#include
using namespace std;
int main()
{
int i = 10;
int& ri = i;
int& rri = i;
cout << &i << endl;
cout << &ri << endl;
cout << &rri << endl;
return 0;
}
:02D8FBE0
02D8FBE0
02D8FBE0
特性:
#include
using namespace std;
int main()
{
int i1 = 10;
int i2 = 20;
int& ri = i1;
ri = i2;//赋值操作,不是引用新实体
cout << ri << endl;
return 0;
}
:20
#include
using namespace std;
int f(int& ri)
{
return ri *= 10;
}
int main()
{
int i = 10;
int ret = f(i);
cout << ret << endl;
return 0;
}
ri 接收到参数 i 后就成了 i 的别名,对 ri 操作 == 对 i 操作
引用作参数的优势:
//传值返回
int f1()
{
int n1 = 1;
return n1;
}
//引用返回
int& f2()
{
int n2 = 2;
return n2;
}
先回忆一下 传值返回…
:创建一个和返回值类型同类型的临时变量(之后称作tmp)==> 将 n1 放进 tmp ==> 销毁栈帧 ==> 通过临时变量带回返回值
这样一来就有一层 拷贝
int tmp = n1;
//销毁栈帧
return tmp;
(tmp小就创建在寄存器内,大就创建在上一层栈帧)
需要十分注意的是:临时变量具有常性!!
n1是开辟在栈上的临时变量,出作用域就销毁,数据不由我们控制了
:相比传值返回,唯一区别就是临时变量的类型是引用
像代码中写到的,也就是把返回值放到一个int&类型的临时变量,返回这个临时变量(n2 的别名)
int& tmp = n2;
return tmp;
显然危险—— n2已经销毁,可得:
实体返回后仍存在,则可以安全返回它的引用;反之不行
引用返回的优势:
const引用,常引用,引用的实体为常量(具有常性)的引用
要学const引用,首先得知道这一原则:
指针和引用的赋值中 权限可以缩小或平移,不能放大
为什么就是指针和引用?它们都影响实体,如果不影响,就不涉及权限…
int main()
{
int a = 10;
const int* pa = a;//pa是指针,影响a,报错
const int tmp = a;//tmp只是普通的变量,不影响a,正常
return 0;
}
int main()
{
int a = 10;//int 可读可写
int& ra = a;//int& 可读可写
//a ==> ra
//int ==> int&
//可读可写 ==> 可读可写 权限平移,没毛病
const int& cra = a;//const int& 只读
//a ==> cra
//int ==> const int&
//可读可写 ==> 只读 权限缩小,没毛病
const int b = 20;//const int 只读
int& rb = b;//int& 可读可写
//b ==> rb
//const int ==> int&
//只读 ==> 可读可写 权限放大,error
const int& crb = b;//const int& 只读
//b ==> crb
//const int ==> const int&
//只读 ==> 只读,权限平移,没毛病
return 0;
}
现在再看一个传值返回的例子:
#include
using namespace std;
int f()
{
int n = 10;
return n;
}
int main()
{
//传值返回,产生临时变量
int& ret = f();//error
return 0;
}
要求给int&的初始值必须是可以修改,难道不是吗?
其实
都会产生临时变量
而,最最重要的还是,这个临时变量具有常性,不可修改!
#include
using namespace std;
int f()
{
int n = 10;
return n;
}
int main()
{
//类型转换,产生临时变量
double d = 10;
//int& rd = (int)d;//error
const int& rd = (int)d;//ok
//传值返回,产生临时变量
//int& ret = f();//error
const int& ret = f();//ok
return 0;
}
哦哦哦,懂了,然后呢,有什么用?
不好,传参会受限制,可能会权限放大——比如形参是可读可写的引用,实参传了只读的实体
那怎么做?
const引用不就来了嘛,形参设计成const引用,你不管传什么,都是 权限缩小或平移
#include
using namespace std;
int f(const int& e1)
{
//...
}
int main()
{
int a = 10;//缩小
const int b = 20;//平移
f(a);
f(b);
return 0;
}
既然它和指针这么像,我们就好好看看它们到底有什么区别,上反汇编…
lea(load effective address):加载有效地址
mov:类似赋值操作
二者都是
可知,引用底层实现上是占空间的,也可知,指针和引用的底层实现是一样的。但是语法上,引用还是不占空间
C语言中,对于规模小、结构简单、重复调用的小函数,总是开辟栈帧开辟栈帧,很浪费性能,C++提出内联函数来解决这个问题
inline是C++中的关键字,只是建议编译器将某函数视为内联函数,编译器会根据情况来决定
特性:
1. 内联函数在预处理后会在调用函数的地方直接展开,不会开辟栈帧,而是直接执行指令
*也代表如果内联函数的规模大/结构复杂,会造成代码膨胀:比如递归,疯狂地展开一堆代码,最直接的体现就是最终出来的exe文件会变得很大
2. 不适合声明定义分离
直接展开,代表内联函数没有函数地址,不会进符号表,链接时会产生链接错误
// F.h
#include
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
应用:小函数
本质上,inline是一种空间换时间的做法
问:宏的优缺点?
优点:
缺点:
问:如何解决宏的缺点?
1. 换用const enum来定义常量
2. inline 小函数
auto,一种数据类型,可以根据被赋的值自动推导类型,是C++的关键字
为什么会出现这样的数据类型?学到后面,会发现类型的命名简直复杂:
#include
#include
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
std::map
typedef char* pstring;
int main()
{
const pstring p1;
const pstring* p2;
return 0;
}
typedef要求我们 声明变量的时候必须知道之后会给它赋什么类型的值,有时候很苦恼, 那看看auto
std::map<std::string, std::string>::iterator it = m.begin();
auto it = m.begin
舒服
int main()
{
int x = 1;
auto a1 = x;//自动识别
auto a2 = &x;//自动识别
auto* a3 = &x;//指定auto为指针类型
auto& a4 = x;//指定auto为引用类型
//typeid(variable).name():拿到变量类型名称的字符串
cout << typeid(a1).name() << endl;
cout << typeid(a2).name() << endl;
cout << typeid(a3).name() << endl;
cout << typeid(a4).name() << endl;
return 0;
}
:int
int *
int *
int //语法上来 说,x是int类型,它的别名找到的一小块内存空间也是int类型
1. auto声明引用类型时,必须指定auto为引用类型(否则会直接识别为原类型)
int main()
{
int x = 1;
//auto声明/定义指针类型,指不指定都行
auto a1 = &x;//自动识别
auto* a2 = &x;//指定auto为指针类型
//auto声明/定义引用类型,必须指定
auto a3 = x;//自动识别为int了
auto& a4 = x;//指定auto为引用类型
cout << typeid(a1).name() << endl;
cout << typeid(a2).name() << endl;
cout << typeid(a3).name() << endl;
cout << typeid(a4).name() << endl;
*a1 = 10;
cout << x << endl;
a3 = 20;//a3与x无关
cout << x << endl;
a4 = 30;
cout << x << endl;
return 0;
}
2. 一行声明多个auto变量时,整行的变量类型必须相同(auto根据第一个变量类型,确定后续的类型)
int main()
{
auto a1 = 1234, a2 = 123.4;
return 0;
}
3. auto不能作为函数参数
函数开辟栈帧前,要先根据参数计算要开辟多大空间,但是auto大小不确定使得编译器无法计算
void TestAuto(auto a)
{}
4. auto不能直接声明数组
int main()
{
auto a[] = { 1, 2, 3, 4 };
return 0;
}
5. 为了不和C++98的auto混淆,C++11的auto只作为类型指示符
范围for,自动判断范围,自动迭代
使用时,需要给一个迭代变量,再给定迭代范围,每次迭代都重新创建迭代变量
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto e : arr)//迭代变量叫啥都可以,e代表element
cout << e << ' ';
cout << endl;
return 0;
}
:1 2 3 4 5 6 7 8 9 10
1. 迭代时,编译器每次取范围内的数据,赋值给迭代变量,迭代变量并不改变范围内数据
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto e : arr)
e *= 2;
for (auto e : arr)
cout << e << ' ';
cout << endl;
return 0;
}
:1 2 3 4 5 6 7 8 9 10
如果想改变,设计成 auto& e
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto& e : arr)
e *= 2;
for (auto e : arr)
cout << e << ' ';
cout << endl;
return 0;
}
:2 4 6 8 10 12 14 16 18 20
用auto*可以吗?不行,范围for只是每次取 arr[0]、arr[1],是int类型,不能用int*接收(引用还是有意义滴)
2. 迭代范围必须是确定的
如果迭代数组,范围就是第一个元素到最后一个元素
如果迭代类,要提供begin( )、 end( ) 两个方法
//数组传参后变指针,范围不确定了
void TestRangeFor(int[] arr)
{
for (auto e : arr)
cout << e << endl;
}
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
TestRangeFor(arr);
return 0;
}
3. 迭代的对象要实现++和==的操作。(现在做个了解,以后才讲的请)
学习了这么长时间,多多少少知道NULL本质上是标识符,但是它在传统C的头文件(stddef.h )中的定义是这样:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中,它的定义出现了bug,被定义成了字面常量0,这也导致许多地方的使用出现问题:
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(int)
f(int)
f(int*)
我们希望NULL是void* 类型的指针空值,但却被识别成字面常量int类型的0,想正常用还要强转,很麻烦。这么简单的bug,C++委员会怎么不修复?
语言要向前兼容,有些代码就按照这个特性跑得好好的,你修复,我代码崩了,我还能好受嘛。所以只能打补丁:
用 nullptr 来代替 NULL 的功能
nullptr 就相当于 (void*)0
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(nullptr);
return 0;
}
:f(int)
f(int*)
注意:
今天的分享就到这里,感谢大家能看到这,不足之处多多交流
这里是培根的blog,期待与你共同进步!