上一篇文章中,我们讲了一些关于编译和链接以及基础的C++知识。详情请见文章
SecureCoding in C and C++(二)
本篇文章将从循环开始写起
1 循环
1. 1 for
先来个简单的例子:
打印hello world 五次:
很简单的吧
for循环的语法是这样的:
for (变量名,对变量的限制,变量自身的变化)
实际上,for循环先对第一个参数进行运行,比如常用的int i
随后在for循环结束后进行评估,来确定第二个参数的bool值,随后执行最后的自身变化。
当然,只要你的变量在一开始声明,也不必要在for循环的第一个参数内声明:
如:
当然,我们也可以对其第二个参数进行提前的声明:
只不过这个时候对循环是否可以进行下去的判断逻辑就到了循环内部里面了。
所以还是跟我的上一篇文章一样,C++很自由,不要被传统的约定俗成的所限制,要大胆发挥自己的想象力!
1.2 while循环
while循环的语法是这样的:
while (bool)
只要括号里面的条件一直被满足,则一直进行下去。
那么对于两种循环来说,如何去选择呢?
假设你正在做一款游戏,这款游戏有一个功能是一直需要运行的,如玩家的框体,一直处于running状态,这个时候就选择while
当我们在处理定长数组时,就选择使用for循环,因为for循环可以对循环的次数和情况做出更加精细化的处理。
1.3 do while(不是很常用)
语法:
do {
<#statements#>
} while (<#condition#>);
例如:
2.控制流语句
2.1 continue
这里使用for循环来掩饰 continue语句
continue将跳到for循环的下一迭代。
示例:
如果i可以%2=0,那么就跳过这个循环,进行下一次循环。
因为我们的i是从1开始的,所以结果应该会打印 1,3,5
因为2,4可以%2=0,所以这里就进行了下一次循环。
结果如图:
进一步去理解一下:
我们改写这样:
结果显而易见了。不大于2的时候会执行Log打印。
2.2 break
就跟单词意思一样,休息一下
这里我们使用第一个例子:
当我们的i为2的时候,if判断为true 执行了break ,让循环休息了
所以这里指打印1,一点也不奇怪。
ok,让我们稍微break一下
2.3 return
对于控制流语句而言,return可以在任何语句中使用,可以在函数中用来返回值/或者其他东西
这里就不再详细的讲解了。
跳过,把篇幅给比较重要的下一个章节 -指针
3. 指针
3.1 指针的概念
这里讲解的指针式原始指针,对于智能指针来说,后续会讲解。
指针其实很简单,一句话吧:指针是一个整数,是一种储存内存地址的数字,就是这样。指针就是地址。就是这样
指针的类型并不会改变指针的实质,不管是int还是其他的,实质就是:指针只是一个内存地址。
例如:
void*ptr =NULL
在这里定义一个空指针。
NULL的含义其实就是
#define NULL 0
是的,这里就是让定义的指针ptr 为空指针,也就是内存地址为0.
也可以这样写:
void * ptr = nullptr;
这里定义了空指针。
定义格式:
type * name
让我们来做一个简单的练习:
在一个变量前面加入一个 “&“符号,表示去取这个变量所在内存的地址:
这里使用&去取整形变量var的地址值,给指针ptr
下断点debug我们可以得知:
ptr内存储的就是变量var所在的地址。
好的,那么我们清楚了,如何去取某一变量的值到指针中,是“&”
那么怎么去逆向的使用指针呢?如何利用指针去修改他呢?
使用“”
是的,就是一对正反操作:
这里我们来试验一下:
报错咯,因为我们在定义指针ptr的时候,是按照空指针去定义的。所以这里我们去写数值的时候,得把指针类型改为int才行。
这里做如下修改:
运行结果理所当然是12
让我们去进一步的查看这个过程,在var和ptr处下断点:
一开始,var的内存地址和其值如图所示
随后执行到var =8时,
内存里变为
这里还可以去印证一件事情:
ptr的内存和*ptr的内存:
ptr的内存时指针变量所在的地址,&ptr是指针变量所指向变量的值。
即:这里的ptr指向的是变量var
理所应当的ptr的值是var变量的地址
也即是16FDFF2BC
去看一下ptr在内存里的情况:
可以看到这里是高地址存放高位,连起来就是 016FDFF2BC 就是var的地址。
而ptr本身的地址是前面的: 16FDFF2B0
这样就很明显了吧
让我们做一个测试去印证下:
我们知道指针的本质其实是地址,让我们再来创造一个指针add
用来显示ptr指针本身的地址:
这里对变量做出解释:
首先我们定义的整数变量var 是8
这里使用了ptr指向了var。
ptr指针的值,也就是var所在的内存地址。
随后我们使用了指针add去指向ptr,这里由于ptr本身就是一个指针变量,这里就相当于赋值了,将ptr内的var的地址的值,赋值给了指针变量add
接着使用。ptr_add变量,去指向了指针ptr所在的地址,(使用了&),这样ptr_add的值,就是指针变量ptr的值了。
与上面debug的结果十分吻合。
3.2 指针的用法
首先我们来看一下接下来的操作,我们向内存去申请一部分空间,这里常用的是用char指针去申请堆内存,因为char是一个字节:
示例如下:
#include<iostream>
#define LOG(x) std::cout<< x <<std::endl;
#include "Log.h"
int main()
{
char * buffer = new char[8];//用char类型的指针去使用8字节的空间,并将空间的开始地址返回给buffer指针。
memset(buffer, 3, 8);//这里的buffer其实存储的就是区域的起始地址。
std::cin.get();
}
在我们运行之后,打个断点,观察其内存空间
这里查看的是buffer指针的值,也就是所申请的内存空间的开始的地址
在我们没有步入line 7的时候,可以看到这部分区域为:
很显然没有执行memset,让我们继续步入:
可以看到内存区域被memset设为了8
也就是8个字节的空间为3;
好,跟3.1讲的一样,我们来试一下双重指针。
#include<iostream>
#define LOG(x) std::cout<< x <<std::endl;
#include "Log.h"
int main()
{
char * buffer = new char[8];//用char类型的指针去使用8字节的空间,并将空间的开始地址返回给buffer指针。
memset(buffer, 3, 8);//这里的buffer其实存储的就是区域的起始地址。
char ** ptr = &buffer;
delete[] buffer;
std::cin.get();
}
这里我们创建了一个指针ptr去指向buffer指针的地址,因此,ptr内的值,就是buffer指针所在的内存的值。
而buffer指针的内容是所开辟堆空间的起始的地址。
让我们来debug一下
先来看buffer指针的内容和其所在内存:
这是buffer指针的内容,左边是buffer指针所在的内存
紧接着我们来查看一下ptr指针的内容:
是的,可以看到ptr指针的内容为016FDFF2B0 存放的就是buffer指针的位置!
很简单哦
这就是指针
最后强调一句:
指针的实质就是地址!!!!
不要想太多
4. 引用
引用说白了,跟指针其实很相似,但是引用仅仅只是引用而已,并没有创造新的变量
变量类型& name
这就是引用
来看一个例子:
#include<iostream>
#define LOG(x) std::cout<< x <<std::endl;
#include "Log.h"
int main()
{
int a =5;
int& ref = a;
LOG(ref);
ref =10;
LOG(a)
}
可以看到,我们通过修改引用ref 去修改了a的值。
让我们来搞一些复杂的东西:
可以看到Increment函数的作用是让value自增。
那么结果是多少呢?
6?
不,结果仍然是5
为何呢?
Increment函数实际上是新建了变量a,然后使其自增。
类似于:
void Increment(int a )
{ int a;
a ++;
value ++;
}
因此这里并没有让a自增。
那么我们应该怎么办呢?
唯一可以做的就是:指针
利用指针的逆写,将修改存入到内存中:
修改如下:
···
#include
#define LOG(x) std::cout<< x <<std::endl;
#include “Log.h”
void Increment(int* value )
{
(value)++;
}
int main()
{
int a =5;
int ptr =&a;
Increment(ptr);
LOG(a);
}
···
这里的括号是因为,在指针变量递增之前做引用,也就是先逆向引用到指针所存储内存的内容,随后自增。
当然,这一小节的标题是引用:
回想一下为什么直接传入a不行呢?其实就相当于创造了一个新的对象。
那么我们直接传入a的引用就好了
#include<iostream>
#define LOG(x) std::cout<< x <<std::endl;
#include "Log.h"
void Increment(int& value )
{
value++;
}
int main()
{
int a =5;
Increment(a);
LOG(a);
}
这里就相当于
int& value =a ;
value++;
引用做的,指针都可以做。指针的功能室比引用强的。
注意:更改引用是可以的!
如:
int main()
{
int a =5;
int b =10;
int& ref=a;
ref=3;
LOG(ref)
ref=b;
LOG(a);
LOG(ref);
}
这样是可以的,
但是在声明时,必须立马指出引用变量是引用的谁:
如果不声明,像这样
就会报错!
这就是引用。
5 类
5.1什么是类
我们终于到了面向对象编程了。
如果你想做一款游戏,那么游戏玩家该如何去设计呢?
玩家有坐标、速度等
我们如果不使用一种将变量聚合等技术去创造玩家,
代码belike:
int Player0X,Player0Y;
int Player0Speed;
int Player1X,Player1Y;
int Player1Speed;
难以维护。。
因此我们使用类
使用calss
当我创建完毕Player后,尝试去改变其内部的属性值,报错了,被修改的属性是私有的。
这是因为我们没有注意可见性。默认情况下,一个类中的变量都是私有的,只有类内的函数可以去访问这些变量。如果我们希望可以在main中去访问,那么应该加入public
如果我们想要Player去移动,应该在class内编写函数,这种在类内部的函数叫做方法:
如
5.2 类和结构体的区别:
类默认是私有的,但是结构体不是
二者的区别在于可见度上
结构体默认为public
由于C中没有类,C++为了兼容C,保留了结构体
所以写了一个define
```#define struct class ``这样就兼容了C中的struct
个人习惯在有大量变量但是没有方法的时候使用struct
反之则使用class
其次就是继承,我只希望我的结构体,是表示一些数据。
5.3 如何去写?
来写一个LOG类
去打印一些调试方便的信息。
#include<iostream>
class Log{
private:
int m_LogLevel;
public:
const int LogLevelError =0 ;
const int LogLevelwarning =1;
const int LogLevelInfo =2;
public:
void SetLevel(int level)
{
m_LogLevel =level;
}
void Warn(const char * message)
{ if(m_LogLevel>=LogLevelwarning)
std::cout<< "[Waring]"<<message<<std::endl;
}
void Error(const char* message){
if(m_LogLevel>=LogLevelError)
std::cout<<"[Error]"<<message<<std::endl;
}
void Info(const char * message)
{ if(m_LogLevel>=LogLevelInfo)
std::cout<<"[Info]"<<message<<std::endl;
}
};
int main()
{
Log log ;
log.SetLevel(log.LogLevelwarning);
log.Warn("Hello");
log.Info("Hello123");
log.SetLevel(log.LogLevelError);
log.Info("123");
}
这个类分了不同的级别,去打印日志,将级别与数字对应,级别越高,数字越小。当然这是一个非常基本的class示例。
6. 静态
6.1 结构体与类外的静态
这里讲解的是结构体外的静态,我们先来看一下:
#include <iostream>
#define LOG std::cout<<x<<std::endl;
int s_var=10;
int main()
{
std::cout<<s_var<<std::endl;
}
这是在main里
我在另一个文件内这样写:
static int s_var =5;
显然这里是以main中声明的为准。
这是因为static声明的变量在link的时候只会从其文件内部进行link。因此这里不会将static的值传入到main里。
如果去掉这里的static
那么只需要在main中的s_var前加入external即可
这被称为external linkage
意思就是在link的时候去文件外找符号。
被static声明后的变量将不会被编译器找到(全局下)
当然对于函数也是一样的。
6.2 结构体中的静态
static其实就是相当于隐形药水的作用。
这里我们引入一个类:
#include <iostream>
#define LOG std::cout<<x<<std::endl;
struct Entity
{
int x,y;
void Print(){
std::cout<<x<<","<<y<<std::endl;
}
};
int main()
{
Entity e;
Entity e1;
e.x=2;
e.y=3;
e1={5,6};
e.Print();
e1.Print();
}
这样当我们没有为里面的变量设置static时,是可以正常运行的。
但是当我们在x,y加入static后
这里就找不到x,y了
这是因为x,y被私有了,也就是静态化了,这个变量只有一个实例,我将其称之为static实例,不属于类的范畴了。
当我们在Entity的命名空间内加入x与y时便可以运行。
但是结果上来看 e和e1的结果是一样的,是因为x与y被static了,这两个变量只有一个实例,而这里就像是创造了两个变量,但是不属于类,因此这里的e与e1实际上指向的是同一个内存空间。
等同于:
Entity::x=xxx;
Entity::y=xxx;
说白了就是加上 static后 ,属性被私藏了。同样的道理,将方法前面加入static也是一样的调用方法:
Entity::Print()
但是当我们将x与y变为非static
Print仍为static时。。
静态方法无法访问非静态变量。
可以这样来理解,在方法加入静态后,此方法不属于这个类了,也就自然的不能访问类里面的变量了。方法只属于static类,这也就自然印证了每个非静态的方法总是获得当前类的一个实例作为参数,但是静态的没有实例呀,所以就无法获得参数,自然也就无法运行。要想使其运行,其实有一个方法:给他传入一个实例就好了:
这里我将在结构体外部编写一个Print
这样就可以成功打印了
总结一句:static的作用是将结构体内的方法/变量 变为只有一个实例,而这个实例只可以在类似命名空间的条件下使用,不属于类的范畴了。也可以理解为加入static后与该类没有关系了。非静态的变量不可以被静态的方法访问,因为静态的方法访问的本质是使用类内的变量进行传参,但是其不属于类,就无法传参。