1.前言
经过上一个小项目的洗礼,如果你真的是0基础,可能会有点怀疑人生的感觉。
所以本章节暂时不做项目了,而是将前面重要的知识点再深入讲解一遍,给大家讲讲有哪些坑,同时也会添加一些后面可能会用到的新知识点。
前面的章节中,我用到了一些东西,在当时为了不过于陷进去,并没有深究,所以首先我们先来将前面提到过的一些东西,在这里好好的理一遍。
2.强制类型转换
如其名字,也就是类型之间的转换,但是要强制进行,如果不强制进行,就不会转换。
首先来看为什么需要强制类型转换,以及强制类型转换到底干了一件什么事情。
现在来观察下面这段代码:
#include<iostream>
using namespace std;
int main() {
char c = '1';
cout << c<<endl;
cout << (int)c << endl;
}
运行结果为:
1
49
这是为什么?明明是同一个变量c
,为什么通过强制转换,将char
转换为int
后,输出的值就不一样了呢?难不成强制类型转换还会改变变量的值不成!
但事实是,强制转换并没有改变任何值,它只是让编译器在解释这个字符c
时的方式发生了变化。
前面我提过一嘴,我们所能看到的一切字符都是通过一张表给映射出来的。
比如上面的代码就是通过ASCII码表映射出来的,更详细的内容可以查看:ASCII码表。
我们首先给字符变量c
赋值了一个字符 ‘1’
,但如果你用鼠标放在这个字符 1
上 ,你就会发现一件神奇的事情:
其实这个字符‘1’,是数字49
强制转换为而来的!
因为‘1’这个字符在ASCII码表中对应的数字就是49
,计算机本身存储的仅仅只是数字而已,就算这里写的char
类型,其本质上依旧只是数字。
唯一不同的地方在于输出,比如当cout
看到这个数字是49
,但由于它是char
类型,所以就将其输出为了字符‘1’,而如果是int
类型,那就会直接根据ASCII码表将其转换为字符‘49’,与数字本身的值相同。
比如:
int a=100;
double b=3.14;
如果像char
那样,直接将数字映射为字符的话,输出int
,就是一个字母,因为ascii
码表中数字100
对应的一个字母。
而double
,这种小数因为没有映射关系,甚至没办法输出成字符到屏幕上让我们看到。
所以为了能够让我们直接能够输出100
,都是需要将上面这些东西进行转换的。
比如数字100
,先拆成 1 0 0
三个字符,然后映射到ASCII
表,分别代表数字 49 48 48
,然后就可以直接根据这三个数字输出对应的字符了,也就能够输出我们在控制台上看到的100
。
综上我们可以得知,所谓强制转换,就是一个东西本来是可以有多种解释方法的,但编译器默认了其中的一种解释方法,这时我们就能够通过强制类型转换,来强制编译器将它解释为另外一种意思。
在C/C++编程中,强制类型转换我们会经常用到,请务必理解其精髓:给目标换一种解释方式。
更底层的原理,你还可以参考本站的另一篇文件:数据类型。
3.输入输出
在C/C++的学习过程中,最重要的一块就是输入与输出,但C/C++
在输入与输出这一块的坑那也是相当的多,且需要理解一些理念。
最常见的坑就是使用scanf
时,变量前面必须带取址符&
,但如果你要接受的是一个字符串又不用加,这是为什么呢?
这就是因为scanf
是根据变量的地址来找内存的,所以变量就得取地址,而字符串属于字符数组,其名字就代表数组的起始地址,所以不用加:
char c;
char buf[30];
scanf("%c %s",&c,buf);
其次就是这些众多的控制符,实在让人望而生畏,所以有了c++中的cin
,其目的就是为了简化这一步骤,它可以自己识别出变量的类型。
但cin
也有缺点,那就是如果输入的东西很多,它就会变得很长,与cout
一样,这就取决于你具体的使用场景了。
而C++中进行格式控制也会相比较于C要繁琐的多,比如要控制输出数字的宽度为5:
#include<iostream>
#include<iomanip>
using namespace std;
int main() {
printf("%5d\n",100);
cout << setw(5) << 100;
}
可以看到,用printf
只需要在%d
之间添加个5
就行了。而C++还需要引入一个头文件iomanip
,然后在输出的时候调用函数setw(5)
。
还有更多的格式控制知识,只能大家自己在网上、平常使用中慢慢积累了。
接下来我要讲的是,输入输出中普遍使用的函数,比如前面我们用到的一个ssprintf_s
函数,可以向字符数组中输出格式化好的字符串。
除了向字符数组,其实还有向文件中格式化输出的函数:fprintf_s
。
这些函数后面带了一个_s
的后缀,说明它是vs中相应的安全函数,如果你使用不带_s
的函数,即当你使用以前的不安全函数,比如fprintf
,那可能就会报错(这个函数不会报错,原因在下),除非你定义特定的宏禁止编译器报错。
那么为什么有些函数是安全的,有些函数被认为是不安全的呢?
这个的主要原因就是,不安全函数不会要求你填缓存区有多大。
比如ssprintf
函数,你只需要填一个字符数组的名字进去就行了,但这个函数内部是不知道这个字符数组有多大的,如果你后面往里面东西加多了,就会越界,最终可能就导致程序直接挂了。
而安全函数主要就是解决了这个问题,当你需要填字符数组时,都会要求你在紧跟其后的一个参数填上这个字符数组有多大。
举个直白点的例子:
//scanf("%s", buf); //不安全函数,不会要求你传入这个缓存区有多大
scanf_s("%s", buf, 10); //安全函数,一般会需要你在紧随其后的一个参数中填入前一个缓存区的大小
使用安全函数时,这一过程并不会有提示信息,你得自己记住填入缓存区大小,如果不是缓存区,比如基本的int
、double
类型,也不用填大小。
所以对于那些本就不需要操作缓存区的函数,比如printf
,其实就是无所谓的,虽然它也有相应的安全函数printf_s
。
上面是C语言的格式化方法,分成了对字符数组、文件、控制台三种操作函数。
除了上面那几种输出,当然还有对应的输入函数,看它们的名字规律,知道怎么写吗?(提示,将函数名中的
printf
替换为scanf
即可)
C++同样提供了三种类对上面三种对象进行操作,分别为iostream
,fstream
以及strstream
,用于格式化输入输出控制台,文件,以及字符数组(又称为缓存区)。
其中,我们经常使用的cin
,cout
是官方已经写好的对象,并且已经定向到了控制台,所以不用我们特地自己去写,可以直接用,而其它两种就需要自己定义使用了。
比如使用前面没用过的strstream
:
#include<iostream>
#include<strstream>
using namespace std;
int main() {
char buf[100]{};
strstream s(buf,100,ios::out);
s << "https://www.kucoding.com" << " " << "余识"<<endl;
cout << buf;
}
其使用方法与文件操作非常相似,除了上面这种可以自己选择参数的,也有特定的类,比如istrstream
,ostrstream
专门用来输入与输出缓存区。
4.缓存区
所谓缓存区,就是申请一块内存,用来专门存数据的,因为char
刚好只占一个字节,便于计算,所以我们一般都是用char
字符数组当作缓存区。
但事实上如果你非要用int
数组当缓存区,也同样可以,因为它们本质就是一块内存而已,怎么使用完全取决于你,C/C++就是这么的自由。
就像下面这样,两种写法本质上都是在申请100
个字节大小的内存而已:
char buf[100];
int buf1[25];
一般来说,申请缓存区有两种方式:
char buf1[100];
char* buf2=new char[100];
delete[] buf2;
上面这两种方法都是可以的,区别仅仅是在使用上。
像第一种这样使用,如果出了所在的大括号,也就是生命周期结束,那么它的内存会被自动回收,之后就无法被使用了。
用官方语言来讲,它是在栈上申请的内存,而且你必须在使用的时候指定明确的数字,这称为静态申请。
栈是一种基本的数据结构,核心机制是后进先出,前面也简单提到过,C++对应的实现有stack
。
而第二种方法,就是在堆上申请的内存,它的生命周围不再局限于大括号内部,只要你不delete
它,那这块内存就不会被自动回收,而且你可以动态指定申请多少内存,这称为动态申请。
还有一点区别就是静态能申请的空间一般没有动态可申请的空间大。
虽然其底层的实现机制可能比较复杂,但核心区别就上面这几点。
就使用而言,你只需要记住它们之间的区别就行了。
当然,这里动态申请内存我是用C++的方式写的,你也可以用C语言的方式(malloc
与free
)。
这两者的主要区别在于对类的申请与释放上,C++
会调用类的构造与析构,而C
只申请内存,其它啥也不干。
更多细节可以学习这篇文章:内存分配。
5.运算符
运算符其实不难,特别是我们本来就经常使用的加减乘除等运算符,所以这里只讲解几个特殊、但又在编程中经常用到了运算符。
首先就是自增自减运算符,即++
和--
。
这两个本身的使用是很简单的,但耐不住总有人喜欢搞事情,比如非要写个++a+b++
这种式子。
这实在没什么意思,毕竟前面我说过,小括号的优先级最高,如果非要写这种别人不容易看懂的式子,最好的办法就是加上小括号。
并且不同编译器,对这类式子的处理方法并不完全相同,所以我的建议就是了解知道这两者的区别就好,没必要过于深入纠结。
如果将两个加号写在前面,则在表达式中自增运算符就会先算,算完之后再算其它表达式。
如果两个加号写在后面,就会先算完表达式,再自增1
。
比如下面这段代码:
#include<iostream>
using namespace std;
int main() {
int a = 10;
int b = a++;
int c = ++a;
cout << b <<endl;
cout << c <<endl;
}
结果为:
10
12
能想明白怎么回事吗?其实很简单,由于第一句++
在后面,所以先进行赋值b=a
,得b=10
,然后再自增1
,此时a=11
。
后一句,++
在前面,所以a先自增1
得到a=12
,然后赋值c=a
得到c=12
。
自减运算符也是同理。
还有一个取反运算符,比较常用,但最开始使用可能有点想不明白,来看下面这段代码:
#include<iostream>
#include<strstream>
using namespace std;
int main() {
cout << boolalpha << true<<endl;
cout << boolalpha << !true;
}
其中boolalpha
为格式控制,用于输出true
与false
,如果不加,就会输出1
和0
,结果为:
true
false
其实际效果就是取反而已,而我们最常使用的就是在判断语句中:
if(!表达式){
}
这种写法,相当于就是如果表达式为false
,就执行。
最后还有或(||
)与且(&&
),这两个运算符主要是用来判断多个表达式的:
if(表达式1 || 表达式2 || 表达式3){ //任意一个表达式为true,就执行
}
if(表达式1 && 表达式2 && 表达式3){ //只有所有表达式都为true,才执行
}
需要注意的点是,这种程序计算机是挨个执行的。
比如上面有三个表达式,就会先执行表达式1,该表达式如果为false
,那么在&&
语句中,后面的表达式2和3就不会执行了,因为已经整体为false
了。
但在||
语句中,却还需要继续执行表达式2,因为目前还不能判断结果。
说这些话的目的是,将最有可能决定整体结果的表达式放在前面,就可以简化运算。
比如三个表达式中,如果表达式1和2几乎总是true
,而表达式3很可能为false
。
那么在&&
语句中,表达式3就应该放在前面,因为如果它为false
了,那么后面两个表达式就不会进行计算了。
6.注释
看到这里你可能会想,注释有什么好讲的,不就两种写法,用于说明代码是干嘛的吗?
是的,注释确实很简单,但有时候你会发现,如果用这两种注释来注释函数,会相当不便携,因为别人怎么知道你这个函数的这些参数是干嘛的,应该填哪些?
所以这就引出了VS中又一强大的功能:自动注释。
比如我们现在有下面这个函数:
int Add(int a,int b) {
return a + b;
}
那么我们只需要在vs中该函数的上一行,连按三个/
,VS就会帮我们自动生成注释:
/// @brief
/// @param a
/// @param b
/// @return
int Add(int a,int b) {
return a + b;
}
然后我们只需要在注释的后面,填写信息即可
/// @brief 用于计算两个数的和
/// @param a 加数1
/// @param b 加数2
/// @return 返回结果
int Add(int a,int b) {
return a + b;
}
brief
:简要说明这个函数是干嘛的。param a
:说明参数a是干嘛的,或者需要填什么。return
:该函数返回值的说明。
然后当我们在使用这个函数时,就会弹出对应的提示信息:
当然,如果你觉得这个自动注释不好看,我们还可以换:
搜索注释,选择C/C++常规,选择批注样式就可以更换,比如我就更喜欢第二种:
需要注意的是,当你选择其中任意一种注释方式,调用方法就变为其后括号中所展示的符号。
比如这里我使用第二种,就需要打下字符/**
来进行注释了:
/**
* @brief
* @param a
* @param b
* @return
*/
int Add(int a,int b) {
return a + b;
}
此时样子就变成了上面这样,但意思还是一样的,其它方式大家也可以试试看,看自己喜欢哪种注释方式。
7.编译选项
从最开始,我们就一直使用的x64
与debug
这两个选项:
但我一直没有解释这两个选项的作用,因为这涉及了一些较底层的知识,远离了学习C/C++基础的目标。
但在后面项目中我们又可能会经常用到,所以我这里还是想再提一下,至少大家有一个印象,知道当我们选择这些选项之后,对于我们有什么用处。
首先来看x86
与x64
的区别。
简单来说,x86
指的是32
位应用程序,而x64
指的是生成64
位的程序,并且目前电脑基本已经过渡到了64位。
那么32位与64位到底是什么意思呢?其实就是指对内存的利用大小。
32位,即232,为4G大小, 也就是说如果你使用32位的程序,你的应用程序最大只能使用4G
的内存。
前面分配空间还记得吗?
用new
就是在分配内存,但这仅仅只是你分配的内存,你还得加上你这程序本身占有的内存,以及去除掉一些不能使用的内存,这就相当少了,一般只有2G不到,不过对于正常应用来说也还是足够了
而64位,则是264,这个数就相当大了,绝不仅仅只是32位翻个倍这么简单,是以T为单位的,1T=1024G,这几乎能够满足当前所有应用程序的需求。
那么如何看自己的电脑是32位还是64位呢?可以来到文件资源管理器,右键此电脑,选择属性:
然后就能看到我的这个电脑就是64位的操作系统:
当然,64位操作系统是完全兼容32位的,这也就是说,如果你生成32位的应用程序,那么在32位以及64位电脑上都可以使用,但如果是64位应用,则只能在64位操作系统中使用。
不过目前64位基本已经普及,所以使用x64
没有太大的影响,下面再来谈一谈debug
与releas
的区别:
首先是debug
,意思是调试,即生成调试版本,程序中会包含很多调试信息,直观的来讲,就是生成的应用程序会很大。
然后是release
,意为发行,即为发行版本,程序中没有调试信息,直观的来讲,就是生成的应用程序会小很多。
至于什么是调试,下一节马上讲。
一般而言,我们在开发一款应用程序时,都会使用debug
版。
因为我们写代码过程中很容易遇到相当多的问题,而用肉眼又很难看出来,这就需要调试了,而并不很在乎应用程序的大小。
当程序写完了之后,我们需要发给其他人用,但现在宽带资源相比之下依旧是很贵的资源,所以应用程序是能小就尽量小,这样用户下载时就能更快下载完成,提高用户体验。
8.调试
调试是程序员最重要的技能之一,如果你以前没有学习过调试,并且在上一个章节是自己一行一行的敲出来的,我相信你一定会遇到一些问题。
程序都已经能够跑起来了,怎么就是展示出来的现象不对呢?然后就一行一行对着参考代码用肉眼找不同。
是不是相当痛苦?
而调试就是为了解决这一问题而出现的,它可以在程序运行过程中,实时追踪各个参数的值,你就可以随时观察哪些参数的值有问题,进而排除错误。
说了这么多,下面来实践一下,代码少,调试起来看不出什么效果,那就用上一章节的推箱子代码来调试一下吧!
首先,将上一个项目推箱子设置为启动项目
然后,我们在它的main
函数里面的第一句命令里面打上断点:
一定看清楚,是在左边灰色上,用鼠标左键点一下就行了,出现红色圆点,就说明打断点成功了。
所谓的断点,就是让程序运行到这个地方,就停下来,让你观察。
这个时候,我们就可以点击调试运行了:
这里注意一下,我们以前也一直点击的上面那个按钮,但其实它旁边也有一个按钮:
这个按钮就是用来直接运行程序的,几乎和你用鼠标去点击生成的应用程序一个效果。
不过我不常用,大家可以自己试一下,这个按钮就不能启动调试功能,即使你打了断点也没用。
点击本地windows调试器后,可以看到,程序就停在了我们打断点的地方。
然后我们就可以通过顶部出现的这些键来控制程序的运行:
其实你将鼠标停在按钮上一段时间,VS就会显示这个按钮的功能名词。
上面所展示的这些按钮是我们调试中很常用的:
- 暂停:将正在运行中的程序停下来(由于目前我们的程序已经停在了断点处,所以该按钮位灰色,不能使用)
- 停止:彻底将程序终结
- 重启:关闭当前程序,然后重新启动
- 逐语句:就是一条语句一条语句的执行,遇到函数,就进函数
- 逐过程:也是一条语句一条语句的执行,但如果遇到函数,就会等待函数执行结束后、在该函数的下一条语句停下来。
- 跳出函数:直接执行完当前函数,也就是跳出当前的这个函数。
我们目前的断点就在一个函数上,当我们点一下逐语句时,程序就跳到了该函数里面:
继续按逐语句,你就能看到一条一条语句的执行,并且控制台上一行一行的字符在被打印。
不过太多了,有点麻烦,所以我们可以直接点击跳出,跳出该函数。
但由于这个函数里面需要我们输入一些东西,如果不输入就会卡着,所以我们得先输入:
点击跳出后,函数中剩下的代码就自动执行完毕了,然后卡在这个函数的下一行。
然后点击停止调试,即那个红色的按钮,程序就会停止下来。