学习cpp 第二部分
如何在vs studio中调试
-
重要的两个内容:断点(break point)、查看内存(reading memory)
-
断点:程序调试中 中断的点, break意味着暂停,在任何代码行中设置断点,当程序运行至此处时将暂停。暂停后可以查看state,也就是查看内存看发生了什么以诊断问题(debug)。程序运行所需要的内存是很大的,包括要查看的变量、调用的函数等。暂停仍然可以查看内存数据。
-
通过打断点可以实现程序逐行运行,在vs中,按F9或者侧边框实现。注意:要在合适的地方打断点,在会被执行的代码上打。另外,要在debug模式下断点,在release模式下,编译器实际上会改变代码。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9ssUxRor-1683883762512)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230419155210711.png)]
点击▶运行,断点上有一个黄色的箭头,右侧出现诊断工具,点击▶可以继续,上面有三个蓝色箭头:逐语句(step into)、逐过程(step over)、跳出(step out)。step into: 进入到该语句,该语句是一个函数Log,则进入到该函数。step over:跳过这条语句,继续下一行代码。step out:跳出该函数,回到调用函数的地方。该例中回到main函数,意味着回到c++标准库中。
- 点击step into:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o293Lwqe-1683883762513)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230419160405522.png)]
页面跳到Log函数的definition处,查看message变量可以看到在本例中为helloworld! 黄色箭头表示代码即将运行到这一行,按下F10(step over)或shift+F11(step out)或F5(继续)就可以运行这一行代码,即打印到console。
- 修改Main.cpp
#include<iostream>
#include"Log.h"
int main()
{
int a = 8;
a++;
const char* string = "Hello";
for (int i = 0; i < 5; i++)
{
const char c = string[i];
std::cout << c << std::endl;
}
Log("helloworld!");
std::cin.get();
}
断点打在int a = 8;
上,可以看见a并没有被创建出来赋值,而是并将其将要设置的内存位置显示出来了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sGbPkGUr-1683883762514)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230419171125711.png)]
在该窗口中有autos,locals和watch。前两个可能展示重要的变量和局部变量,watch观察变量,可以输入变量。后面可以显示内存中的值,可以看到当前变量都是未被定义的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TE4DwOis-1683883762515)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230419171327836.png)]
点击调试 >> 窗口 >> 内存,可以看到被定义的几个内存。输入&a查看其内存位置,为十六进制码,两个数字为一个字节,两个十六进制数与一个字节对齐。大量的cc表示未被初始化的栈内存,调试时看到cc意味着没有初始化这个变量。点击F10,发现窗口中a的值变成8,红色表示自最后一个断点。内存窗口也发生了变化。
继续点击F10,运行到a++,窗口中a的值变为9,继续F10,出现变量string,复制窗口中的值到内存1中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGJ6Bn9A-1683883762517)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230502194938831.png)]
这几个ASCII码代表的含义即为Hello
继续,发现仍在for循环中,内存1输入&c,变量c依次变为Hello。如果要跳出该for循环,可以在for函数之外,Log处打上断点,点击F5或者继续,此时c变量的内存仍然活跃,c值为o。点击F11将跳出整个代码。继续,控制台出现Hello World!,但点击回车无反应,点击继续,控制台关闭。
总结:程序是由内存组成的,无论是指令或者指针,都是代码,都储存在内存中,所以看到内存是非常重要的。通过设置断点,暂停程序,在给定的时间在给定的代码行,查看变量是什么,对代码非常有用。
C++条件与分支(if语句)
-
写代码的时候有时需要一个特定的条件进行评估,根据评估的结果,决定想要执行什么代码。
-
在条件的基础上可以进行分支。此时有两种情况:当运行到if语句时,先是对实际条件语句评估,然后是基于这个条件语句评估后的分支语句,也即是当条件为真时,程序跳到一部分代码中;条件为假时跳到另一部分(源代码)。在运行的应用程序中,指机器指令。
-
开始一个应用程序时,整个程序机器所有模块加载到内存中,也可以说是所有的指令组成了应用程序。
流程:检查条件- > 跳转到内存的不同地方- > 执行指令。条件语句开销大,运行慢,优化代码尽量避免使用条件语句。
- Main.cpp:
#include<iostream>
#include"Log.h"
int main()
{
int x = 5;
bool comparisonResult = x == 5; //存储bool值,即比较的结果。要么结果为真,要么为假。
if (comparisonResult) //省略:comparisonResult = 1
{
Log("helloworld!");
}
std::cin.get();
}
==符号是在C++标准库杯重载了,相当于写一个函数,接受两个整数参数,然后检查这两个整数的内存以确保两个整数相等。即获取4个字节的内存并比较内存的每一位。
-
在if行打上断点并运行,可以看到comparisonResult的值为true;若将x的值赋6,则comparisonResult的值为false,并且断点跳过if函数的主体部分,控制台无输出。
-
详细查看CPU运行了一些什么指令,可以通过编译并查看汇编语言。在断点代码行处右键转到反汇编
int x = 5;
00007FF6DBDA194B mov dword ptr [x],5 //将5移动到寄存器
bool comparisionResult = x == 6;
00007FF6DBDA1952 cmp dword ptr [x],6 //将6移动到同一个寄存器
00007FF6DBDA1956 jne main+34h (07FF6DBDA1964h) //jump not equal,如果两个值不相等,跳到该内存地址中
00007FF6DBDA1958 mov dword ptr [rbp+0F4h],1
00007FF6DBDA1962 jmp main+3Eh (07FF6DBDA196Eh)
00007FF6DBDA1964 mov dword ptr [rbp+0F4h],0 //跳到该行,0是一个偏移量,0加载到寄存器中,赋给bool值。对于bool值来说,0为false,1为true。但只要布尔值一个字节中由任何一位不为0,都是true。
00007FF6DBDA196E movzx eax,byte ptr [rbp+0F4h]
00007FF6DBDA1975 mov byte ptr [comparisionResult],al
if (comparisionResult)
00007FF6DBDA1978 movzx eax,byte ptr [comparisionResult]
00007FF6DBDA197C test eax,eax //将一些值加载到eax寄存器中,测试寄存器是否通过条件。然后对寄存器进行一些逻辑运算。
00007FF6DBDA197E je main+5Ch (07FF6DBDA198Ch) //条件跳转语句,jump equal 如果两个值相等将跳到某位寄存器中。
确保在debug模式下运行。
#include<iostream>
#include"Log.h"
int main()
{
int x = 5;
bool comparisonResult = x == 5;
if (1) //如果条件为真,则运行if语句
{
Log("helloworld!");
}
std::cin.get();
} //最终打印helloworld
- 简化:
#include<iostream>
#include"Log.h"
int main()
{
int x = 5;
if (x==6)
Log("helloworld!");//只有一行代码无需花括号。并且分行,否则断点无法分析CPU运行了什么。
std::cin.get();
}
- if语句只对数值进行检查
#include<iostream>
#include"Log.h"
int main()
{
int x = 5;
if (x) //x不为0则语句为真
Log("helloworld!");
std::cin.get();
}
- 还可以检查指针是否为空(null为0)
#include<iostream>
#include"Log.h"
int main()
{
const char* ptr = "Hello";
if (ptr)
Log(ptr);
std::cin.get();
} //打印Hello
#include<iostream>
#include"Log.h"
int main()
{
const char* ptr = nullptr;
if (ptr) //相当于if(ptr!=nullptr)
Log(ptr);
std::cin.get();
}//无输出
- 加上else
#include<iostream>
#include"Log.h"
int main()
{
const char* ptr = nullptr;
if (ptr)
Log(ptr);
else
Log("ptr is null!");
std::cin.get();
}//输出语句ptr is nunll
- 加上else if
#include<iostream>
#include"Log.h"
int main()
{
const char* ptr = "Hello";
if (ptr)
Log(ptr);
else if(ptr == "Hello")
Log("ptr is Hello");
else
Log("ptr is null!");
std::cin.get();
}//输出Hello
else if语句:只有在if语句失败时才执行,并且相当于else{}
#include<iostream>
#include"Log.h"
int main()
{
const char* ptr = "Hello";
if (ptr)
Log(ptr);
else
{
if(ptr == "Hello")
Log("ptr is Hello");
}
else
Log("ptr is null!");
std::cin.get();
}
- 变成可以分成两类:数学编程和逻辑编程。
Visual Studio的最佳设置
-
创建新项目,解决方案自动设置为和项目一样的名称,但在实际情况中会避免这一现象。
-
创建新项目后,在侧边工具栏中会出现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iC7NVr5F-1683883762518)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230506134332825.png)]
下面一些文件实际上是过滤器,右键项目打开所在文件夹是没有的。
- 在源文件中创建Main.cpp,点击上方显示所有文件,发现这些过滤器没有了,只有
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZV7W2ppx-1683883762519)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230506134609510.png)]
实际上是硬盘里的目录结构。而文件的目录:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jmVrVHye-1683883762520)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230506134742393.png)]
比较乱,这时候可以在VS中创建 一个src文件夹用来专门存放源文件,将Main.cpp移动至src文件夹。如图,同时硬盘中的目录结构随之变化:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JD3GAJpF-1683883762521)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230506134922789.png)]
再次点击显示所有文件按钮,过滤器显示出来,这意味着可以将Main.cpp移动到任何过滤器中而目录结构不变。
- Main.cpp F5编译
#include <iostream>
int main()
{
std::cout << "hello" << std::endl;
}
-
查看与src文件同级的x64文件,内有debug文件,打开却只有中间文件,没有exe文件,实际上在src上一级文件的x64 >> debug中
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GlOGxNIJ-1683883762522)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230506135824334.png)]
-
右键project1>>属性设置中间目录和输出目录,分别是
$(SolutionDir)bin\intermediates\$(Platform)\$(configuration)\
$(SolutionDir)bin\$(Platform)\$(configuration)\
保存后,右键清理,在文件资源管理器中删除无关文件夹,包括刚开始的中间文件等,再次编译,发现中间文件和输出文件都在bin文件夹中。由此实现最佳配置。
- 如果想知道宏是什么,可以在属性>>目录>>编辑中查看
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MtpUI8If-1683883762524)(C:\Users\陈周念\AppData\Roaming\Typora\typora-user-images\image-20230506144155979.png)]
C++循环 (for、while)
-
使同样的工作执行多次
-
可以选择在代码中将函数申明5次,但这样并不方便
-
使用for循环语句:
格式:for ( ; ; )
,括号内由三段组成,第一段,申明变量i的类型以及大小;第二段;语句只要为真,那么将一直执行for循环语句;第三段将在for循环的下一次迭代之前调用
Main.cpp
#include <iostream>
#include "Log.h"
int main()
{
Log("Hello World");
Log("Hello World");
Log("Hello World");
Log("Hello World");
Log("Hello World");
std::cin.get();
}
改为
#include<iostream>
#include "Log.h"
int main()
{
for(int i = 0; i < 5 ;i++)
{
Log("Hello World");
}
std::cin.get();
}
等价于:
#include<iostream>
#include "Log.h"
int main()
{
int i = 0;
for( ; i < 5 ; )
{
Log("Hello World");
i++;
}
std::cin.get();
} //两段声明可以空着,第一段将i在for循环之外首先声明,第三段在for循环内声明
或者将第二段改为:
#include<iostream>
#include "Log.h"
int main()
{
int i = 0;
bool condition = true;
for( ; i < 5 ; )
{
Log("Hello World");
i++;
if (!(i<5))
{
condition = false;
}
}
std::cin.get();
} //没有改变代码的实际行为,第二段实际上是布尔类型。或者第二段只有空格,表示true,语句永远为真,无线打印HelloWorld
- while语句
while( );与for不同,仅有括号内条件判别语句
#include<iostream>
#include "Log.h"
int main()
{
int i;
for(i = 0; i < 5 ;i++) #在for语句中申明的i为局部变量
{
Log("Hello World");
}
Log("==================");
i = 0;
while (i < 5)
{
Log("Hello World");
i++;
}
std::cin.get();
}
- 两者区别
实质上没有区别,看个人喜好。但例如在游戏中存在一个变量running,指让游戏一直运行下去,这时候选择while更好,因为条件并不会改变,不需要在循环前先声明这个条件变量,可以将之前的变量或者函数调用的结果直接拿来用,不需要更新或者初始化某些东西。
使用for的情况:比如有数组的情况,处理10个数组,同时要跟踪某些变量,并且这些变量可以用于处理数组里面的元素,有时用offset偏移量或者索引处理元素。
- 不常用的语句dowhile
do{ }while(条件);与while的区别是不论如何人需要先执行花括号内的循环体一次。
c++控制流语句
- 控制流语句通常和循环语句一起使用,控制循环语句的实际执行,一般有三种,continue、break、return
- continue:只能在循环语句中使用,表示进入循环的下一个迭代,如果没有下一个迭代则结束循环。
- break:用在循环中和switch语句中,表示跳出循环,结束循环。
- return:用在函数中,可以直接退出函数,用法非常广泛。函数需要返回值,如果只有return,返回return本身,return本身只适用在void函数中,函数需要返回时需要提供一个值。
continue:
#include<iostream>
#include"Log.h"
int main()
{
for (int i = 0; i < 5; i++)
{
if (i % 2 == 0)
continue;
Log("Hello World");
std::cout << i << std::endl;
}
std::cin.get();
}
表示i为偶数时直接跳进下一个迭代,执行for语句中的i++,并评估语句中的条件,i为奇数时不跳,直到循环结束。输出:
Hello World
1
Hello World
3
将if语句中的条件改为i>2, 可以类比。输出
Hello World
0
Hello World
1
Hello World
2
- break: 将continue换成break,
#include<iostream>
#include"Log.h"
int main()
{
for (int i = 0; i < 5; i++)
{
if (i > 2 == 0)
continue;
Log("Hello World");
std::cout << i << std::endl;
}
std::cin.get();
}
- 输出仍为:
Hello World
0
Hello World
1
Hello World
2
条件换成i%20,没有任何打印。条件换成(i+1)%20, 打印:
Hello World
0
表示程序一旦到达break语句,循环结束,到下一行语句。
- return:对于举例中的int main()函数来说,return需要返回一个整数。
#include<iostream>
#include"Log.h"
int main()
{
for (int i = 0; i < 5; i++)
{
if ((i+1) % 2 == 0)
return 0;
Log("Hello World");
std::cout << i << std::endl;
}
std::cin.get();
}
控制台闪退,因为并没有运行到std::cin.get();
通过打断点可以看到程序有输出
Hello World
0
并且在第二次循环中到达return 0 ;时直接跳到最后的花括号。
#include<iostream>
#include"Log.h"
int main()
{
for (int i = 0; i < 5; i++)
{
if ((i+1) % 2 == 0)
return 0;
Log("Hello World");
std::cout << i << std::endl;
}
return 0;
std::cin.get();
}
std::cin.get();
这一行永远不会到达,实际上毫无用处,称为死代码。
- 条件语句、循环语句和控制流语句、是唯一可以改变程序行为的东西,不要尝试写入指令指针。