经过上期的环境搭建过后,我们将正式的学习C++系列,首先要学习的是C++的一些常用的变量
从编译和连接学起似乎也是不错的选择。
个人总结的一句话:编译其实就是对预处理语句进行处理后,然后对语句进行处理。对预处理语句,例如:#include 之类的处理方式其实就是将文件内容复制到对应的cpp文件内,随后生成obj文件,然后再经过链接(也就是link)将其处理。
好的让我们开始学习变量:
1. 变量
说起变量,学习过其他编程语言的小伙伴们其实很清楚,大致上也就那些,什么int char string float这些基本都是常见的了,但是这里我想说的是,其实他们去做什么作用我并不关心,就像一个人的身高和体重,利用整数和浮点数来表示我不是很关心,我关心的只是不同变量的大小之分。
来举个例子:
#include<iostream>
int main()
{
int v =8;
std::cout<<"hello world !\n" <<std::endl;
std::cout<<v<<std::endl;
}
这里我们明显的使用了整型,也就是int
1.1整型变量 int
整型变量的作用是在一定范围内存放整数
占的大小是4个字节,是一个有符号变量,其空间大小为-20亿到+20亿
4Byte =32bit 其中的一位占做符号位,也就是实际上来说的31位表示数据位。
那么我们也可以让其始终为正,来表示全32位都是有效的数据的变量,这就引来了无符号整型:
unsigned int
这样看来,其32位全部可以用0或1来表示,因此他的大小为20亿的两倍,大概为42亿。
1.2 字符变量char
char
占1位
其实,我起初所说的对变量真正期望你使用其来表示什么数字其实并不关心,原因就在char里
其实char是用来表示一个字符的,但是你也可以用其来表示数字。
举个例子:
#include<iostream>
int main()
{
char a ='A';
char b =65;
std::cout<<a<<std::endl;
std::cout<<b<<std::endl;
}
在我们进行build之后,运行结果为:
那么为什么呢? 为什么char b输入的是数字,但是实际上出现的是A呢?这其实就是Ascll
说白了其实就是一些约定,所以在C++使用的过程中,我们没必要去让自己遵循一些既定俗成的约定,没啥必要,我们要发挥自己的主观能动性!
变量与变量之间唯一的区别,个人认为是在变量被创建的时候分配了多少空间
剩余的变量就不一一介绍了
short
2字节
long
4 字节
long long
8字节
1.3 float double
float
为4字节
double
为8字节
float变量在定义的时候要在其后面加入f
float c=5.5f;
double d=5.2;
1.4 Bool
bool,学过js肯定都知道吧,其实就是boolean 用来返回true或者false
但是当我们使用cout去打印bool变量的时候
会出现的是:
是因为计算机并不知道什么是真的,什么是假的,他只知道0是0
因此bool里除了0表示假的,其余都是真的。
但是bool也跟char一样是占一个字节的。
有人可能就问了:
为什么不是一个bit呢?既然只表示0或者1,1位不就够了吗?
确实是这样的。1位确实就够了,但是当我们的CPU去寻找这个bool值的时候,如果这个bool为1位的话,我们没有把那去寻址只有1一个bit位的内容,ds和【】都需要2个字节。所以我们寻址的时候必须得按字节来寻址,经过cs的左移之类的。
因此我们不能去创建只有1bit的bool,我们要寻址。但是我们可以在一个bool内创建8个bool值,1bit1个。这都是后话了
我们也可以用sizeof去查看对应的变量类型占了多少个字节。
如下:
运行结果如下:
int占4字节、double 8字节、float4字节、char1字节 long 8字节 long long 8字节 short2 字节
1.5 总结
常用的变量也就是以上的内容了,剩余其他变量可以用他们进行组合,当然我们也可以让其变成指针型的变量或者引用。这都是后话了。
2.函数
什么是函数?其实就是一个被我们设计去实现某种功能的代码块罢了。也解决了实现某个功能,我们不必的去重复写的麻烦。函数有几个值得注意的地方:
- 函数的返回值
- 函数所要传入的参数
例子:
int Multiply(int a,int b)
{
return a*b;
}
这个函数的作用是返回两个整数的乘积,所以其返回结果是 int 也就是 写在了函数一开始的地方,随后传入了两个整型int变量。
那么如何去调用这个函数呢?
我们在main函数中定义两个整型的变量即可:
函数其实还是比较简单的哈(
3. 头文件
头文件说白了就是一个用来声明函数的文件。
举例:
为了输出服务器的日志方便,我们需要一个Log函数去打印服务器的日志到终端里:
这里我们编写一个十分简单的函数Log,
void Log(const char * message)
{
std::cout<<message<<std::endl;
}
void InitLog()
{
Log("Initializing Log");
}
我们将log放置在一个叫Log.h的头文件内;
在我们的main函数所在的文件内未进行#include “Log.h”的文件声明之前,我们是无法在main函数中使用的
#include<iostream>
int Multiply(int a,int b)
{
return a*b;
}
void MultiplyAndLog(int a, int b)
{
std::cout<<Multiply(a, b)<<std::endl;
}
int main()
{
int a=1;
int b=2;
std::cout<<Multiply(a, b)<<std::endl;
MultiplyAndLog(2, 3);
Log("hello");
InitLog();
}
我们将main函数所在文件main.c中尝试使用Log.h中有的函数,并且在Xcode中进行编译:
会报错:无法匹配到Log的函数调用和不清楚的InitLog定义。
这是因为我们没有对其进行声明;也就是没有调用Log.h的缘故
在我们加入之后,程序正常运行了。
当然,我们也可以这样:
在一个其他的文件内,编写log函数:
这里我们在一个单独的cpp文件中编写了log和InitLog函数,我们可以在main函数所在的cpp里进行声明:
可以看到成功运行了:
这里进行声明的时候,只需要将对应函数的 返回值,名称,传入参数输入即可
如这里的:
void Log(const char * message);
当然我们也可以在头文件中进行声明,随后在main.cpp中调用头函数
这样也是可以的。
但是有个问题需要深思:
3.1 头文件中 #pragma once的运用
我们知道cpp文件在进行编译的时候,其本质就是将预处理,也就是带“#”后面的内容进行复制粘贴行为,因此假设我们在处理一个含有结构体的头文件的时候,我们一不小心在main函数所在的cpp文件内进行了两次重复的调用,那么我们就生成了两个一模一样的结构体:
例如:
然后我们在main中引用了两次Log.h
这个时候就会报错。
错误为:
Redefinition of 'People'
这是因为预处理的特征是复制粘贴,导致了两个相同的结构体出现了。
那么我们需要在头文件中加入#pragma once 进行处理,这个预处理语句的作用就是防止头文件的内容被重复的调用,导致出现 Redefinition 的错误。
3.2 ifndef的运用
除了3.1中所提到的#pragma once
我们还可以利用预处理的特性来解决这个问题:
即在头文件中加入:
#ifndef _LOG_H
#define _LOG_H
#enif
来进行判断,其原理是:在编译器进行预处理的操作时,首先要将预处理的文件复制,然后粘贴到调用中:
这里进行复制时,会出现以下情况:
ifndef _LOG_H
define _LOG_H
你的代码:
endif
会首先执行:ifndef判断 判断是否定义了_LOG_H
如果没有定义,则进行定义 随后执行你的函数
如果定义了,那么直接进行endif。
来尝试下,例子如下:
这种情况下,即使在main中调用了两次的Log.h也不会出现错误:
还有一点需要注意的是 #include “”引号表示的是 相对于当前文件的情况,属于相对路径
而#include<>使用的是绝对路径
文件头也很简单呀~
4. 使用IDE来调试
4.1 使用VisualStudio
- 设置断点:
直接按F9就可以设置断点
再点击一次红点就可以取消断点
随后确保在debug模式下 点击第二个红色箭头所指的地方进行debug调试
点击后,界面会发生变化
这里有三个调试步骤
- 逐语句 (step in) 进入当前函数,这里是进入Log函数 然后从Log函数里进行其他的调试 快捷键 F11
- 逐过程(step over) 跳过当前函数执行下一行,即不进入Log函数,直接调试下一行 快捷键F10
- 跳出(step out) 跳出当前函数回到断点处,退出调试模式. 快捷键shift + f10
- 读内存
我们按照第一部分的debug 进行
当我们按F11 进入到 Log函数中
可以看到 message的内容为"Hello world !"
好像有些许简单了,让我们来加入一些其他的变量来增大难度:
#include<iostream>
#include "Log.h"
int main()
{
int a = 8;
const char* string = "Hello";
for (int i = 0; i < 5; i++)
{
const char c = string[i];
std::cout << c << std::endl;
}
Log("Hello World!");
std::cin.get();
}
新的代码后,我们不加断点直接进行运行来查看一下结果:
然后我们把断点设在int a处;
在未步入之前 a是多少呢?
为什么? 我设定的a明明是12403 啊
因为我们还没有进行执行 int a的指令,换句话说 这里只是cpu通过 cs:ip 读取到了这句 类似于 mov ax,8
但是还没有进行执行,ip还没有+3 的情况下.因此这里的值是未初始化的 a的值.所以是正常的.
一直步入到第6行的时候,再进行步入的时候才发现右下角中的string出现了值
这里要介绍下调试的窗口:
- 自动窗口
这里面可以看到IDE自动给你生成的名称以及其对应的值和类型
- 局部变量
这里看到的是程序运行过程中所用到的局部变量的值 - 监视1
可以通过箭头所指方向进行添加所要监视的变量.
- 内存读取
使用后可以查看内存指定位置的值
这里查看了Hello world
也可以使用 &变量名来查看变量
&a来试一下
这里可以看到就是a的值 08
如果看到的是CC
那么说明这段部分还没有被创造栈,也就是这个变量没有被初始化.
4.2 使用Xcode
这里我们使用Xcode来进行调试,首先把程序补全:
#include<iostream>
#include "Log.h"
int main()
{ int a = 8;
a++;
const char * string ="Hello";
for (int i =0 ;i <5 ;i++)
{
std::cout<<string[i]<<std::endl;
}
Log("Hello world!");
std::cin.get();
}
然后我们来看下Xcode的debug功能:
- step in 也就是 F7 步入 就是进入函数内部进行debug
- step over F6跳过此函数 执行下一行
- step out F8 跳出
- create breakpoint currently 是comman +\ 是精确的创建断点
相较于Vs来说 Xcode的debug模式是全一些的 甚至可以通过 comman + Y来激活/不激活某处断点。
这里还是老样子在 int a处设下断点
这就是Xcode的调试界面了,与Vs不同的是,得手动点击播放按钮后才可以开始调试。
我们分两个区域来查看
第一个区域是左侧栏
这里显示的元素有 程序运行进程的pid 占用内存 以及 程序运行的线程 和当前调试的程序名:main
点击main的下面会出现当前程序运行的情况
由于博主的MacBook是M系列的,因此跟Intel的指令读起来还是有些许不同的,不会存在常用的ax,bx等通用寄存器。这里就不必多言,关注的是debug的过程和思想。
让我们来看另一个区域,就是debug区域的下方:
这个窗口跟VS的类似,有auto 、local 和all
auto就是自动 local是本地的变量 all是都显示出来
与VS不同的是 这里的监视器写在了左边
对于查看内存来说,我们只需右键想看到的变量的名称来进行查看内存
这里查看的是string
结果如图:
可以看到,内容就展现在了右边。
看起来还是Xcode的debug展现的功能比较全一些,这也为我后期对VS的优化做了铺垫。
5. 常见的语法
5.1 if else语句
先来看一下简单的if else的示例
这里将Compar之后的结果 存储在bool变量里,可以直接方便的去套用if语句
我们更应该关心这个if 语句在实际的运行过程当中发生了什么
让我们来运行一下:
当然,显示了hello world !
当我们把 x 改为6
什么也没发生。这是当然啦
让我们在 int x的地方下断点
当我们一步一步的去步入的时候,ComparisonResult为false;
因此就退出了。
让我们去VS里面看一下汇编是怎么写的(别问为什么不用Xcode看,问就是没学过arm的)
首先使用mov 将 6放入 寄存器里
随后开始判断ComparisonResult的值
也是将其放入寄存器里,随后使用jne比较(jump not equal) 这里看出 x并不是为5 实际上是6 则跳转到了 07FF742BE6825h那边
最后因为不相等,就跳转到了 cin.get()
再来看一下if语句对应的汇编吧
这里的语句实际上是,将ComparisonResult的内容进行比较,然后执行je(jump equal) 相等就跳转 实际上比较的是是否与false相等:
这里因为不是false,所以就不跳转,转身去执行下面的log函数
实际上我们可以进一步的简化代码,使其更加清晰。
在这里可以这样进行简化
众所周知,只要是bool不为0
那么都为真,这里直接写成x就很合理了。
对于指针我们也是可以这样写的。
例如:
这样对于指针来说,只要不是空指针或者0,就可以成功的调用Log()
可以看到已经成功打印了。
那么如果我们将其设为nullptr,则不会打印。
就是这样!
好了由于时间的关系,这篇文章最终以一万字结束。期待下一篇,下一篇中,我将继续带来关于C++相关的精华部分,指针、引用、类与结构体等待。
喜欢的话点个关注,求个赞赞和收藏~~