①gcc的编译操作指令:
②设置Path的方法:右键windows图标,选择系统,右侧选择高级系统设置--点进环境变量里面即可添加&编辑环境变量
③前置知识的一些补充:
1.八进制&十六进制存在的意义:单纯二进制表示数字的话位数太多太过繁琐,由此可以用三位/四位二进制数映射成一个数字来表示。实际上,十六进制其实更好,表示起来更方便
※PS:以前使用八进制多,现在使用十六进制多的原因:
2.遗留一些问题,后续解决:
这两个关键词的意义?应用场景?
3.对于关键字的分类思路:
(1)内存分配行为单词--包含基础数据类型关键字,int,char等
(2)逻辑处理行为单词--包含分支处理行为关键字(switch&default&if等)与循环处理行为关键字
(3)自定义数据行为单词--包括enum&struct等
二、一些另类写法指针:①引入:空间的操作方式--有名访问&无名访问
※②指针的要素:一个是大小,一个是访问的方式,更通俗一些可以称为步长
大小——往往不那么值得关注,无非就是32位4个字节,64位8个字节,于是我们更该关注的是指针的访问方式
拿int*p 和char* p作比——决定p大小的是*符号,决定了它是个指针,再依据操作系统来分配大小。由此前面的int&char这样的数据类型决定的其实是这个指针的访问方式,也可以理解为这个指针每次访问移动的步长。于是,指针+1,+5等,其实是加了1个&5个步长
※所以,如果有char*p=100这样的赋值,是不对的。虽然这样指针可以知道它的大小,知道它当前的位置,但是它没法获得步长信息,也就是如果指针++或者--等,是无法正常完成访问的。
③阅读一些另类指针的方法:
比如int* a4[5]---先往右看,a4升级为数组,把整个a4当作整体,紧接着往左看,左边要回答的是每个元素的类型--发现是int*,于是a4代表的是一个五个元素的整形指针数组--也就是说,*修饰的其实是a4[i],而不是a4
再如int (*a5)[5]---先往右看,发现遇到了括号,不读,往左看,发现有*,于是a5先升级为一个指针。升级为一个指针之后,回答两个问题,一个是大小,一个是访问方式。先往右看,发现是[5],所以访问空间就是五个五个的访问。再往左看,回答的问题是五个元素中每个元素是什么,发现是int。因此,a5访问空间的方式是一次跨过5个int
再如int a6[3][4],其实就是个线性组合。一共有三串东西,每一串有4个int,a6即是一次访问4个。a6和a5的区别,只是a6指明了一共会访问三串,一串5个int。而a5没指明一共访问几串
再如int *a7[3][4],就是一个元素为int*类型的,三行四列的数组。
再如int (*a[5])[4],先看a,a是一个有五个元素的数组,每个元素里放了一个指针。紧接着回答每个指针的问题--向右看,每个指针的访问方式是4个4个的访问,每个是int。或者更通俗一些--其实就是a代表了五个抽屉,每个抽屉里存了一把钥匙。那把钥匙的访问方式是4个int4个int的走
再如int a9[5][3][4],先看[3][4],是一个3*4的访问方式,然后一共五个这样的平面
再如int(*a8)[3][4],同样的a8先升级指针,然后回答问题,怎么去访问--发现是一个面一个面的访问(3*4的面,每个是int)
※既然:指针+1,是走一步,所以对于a9&a8,+1就是挪到下一个面,(当然也是线性存储的,其实就是前移3*4*sizeof(int)个字节的地址)
④C与Cpp在内存访问方面的一个比较大的差异——C实际上是允许一点一点的访问内存的,但是Cpp不允许,Cpp要求你必须按照步长一步步的走。
比如:
int a[5][3][4]; char *p=a;
这样的写法在C中是允许的,这样之后我们实际上就只关心p这个指针的行为了。就像一个一块50平米,有4块这样的地的一共200平米的房子,我们却非要去一平米一平米的来访问——C允许这样,但Cpp不允许。Cpp必须需要你用同样级别的指针来相互赋值。
这其实也体现了C和Cpp“面向”的不同——Cpp更加面向业务,所以他需要严格的类型检查。而C面向底层,它的标准也非常开放,使得使用者拥有极大的自由。同样地,也保证了访问内存的自由。
三、函数指针:多态的底层实现方式譬如这样的一个例子:
函数名的本质,是一个很特殊的地址值!
①定义:拆解来看,先是中间:(*p),括号括起来表示它是一个指针,然后右边描述它代表的函数的传参,左边描述它代表的函数的返回值类型。同样的,按上面的说法,左边右边的东西,是用来修饰“抽屉里的东西”的。
有的时候如果想要有指向库函数的函数指针,可以直接去库文件里面复制库函数的声明——
※函数名本身是个常量,但是函数指针可以实现变量的效果!
②内存空间相关的一些补充知识:
(最底下的VA的意思其实是Virtual Address,也就是虚拟地址)
比较重要的:
(1)栈--要注意变量出栈之后,原本的内存空间就不再受保护了,即变为可用的。后续为局部变量分配空间的时候就可能分配到这个地方——因此不要返回局部变量的引用等!
但是变量是否是严格的先进后出来入出栈,实际上是编译器的行为,不太好笃定——
我们观察a最终以十六进制形式输出的结果为12345699就可以看出,buf[4]占掉了a的一部分内存空间。但是就像前面说的一样,编译器不一定会规规矩矩的挨得这么紧凑的玩栈,所以如果程序异常,不要通过这种的值的改变来推断,仍然应还是从防止下标越界这个方面来防止程序的BUG!
同样地,我们观察这个程序,发现a明明指定的是const,但是还是被篡改了——这是因为C中的const关键字只在编译阶段保证变量的值的一定,在运行阶段就无法保护了。
不过Cpp中的const是真正的const——
(2)data区--尤其是全局(定义于函数块外)&静态变量所在的区,生命周期是整个项目。因此如果有这样一段代码:
void func()
{
static a=5;
a++;
}
int main()
{
}
如果多次调用func函数,从第二次开始,a就会在之前的值上++,而不是重新赋值为5——一定要区别于下面的这种写法。下面的这种写法是赋值操作,自然会不断地赋上5;而上面的写法,是初始化操作,从第二次遇到开始,编译器就会自动把这条语句当作声明语句,而不会重新再定义&初始化一遍a。
void func()
{
static int a;
a=5;
}
int main()
{
}
这个例子还有另外一个看待的方面——理论上来说,但从变量在内存中的存在方式来看,main块是完全可以访问这个a变量的,只是没有一个名字给main访问。
※也就是说:a这个变量是全局的,但是a这个名字,是局部有效的
rodata--read only data,亦即可读数据区,和代码区都是只读的——代码区要是会被乱改的话,整个程序就乱套了(这里的改指的不是这样敲代码来更改,指的是无法通过一些其他的奇奇怪怪的方式修改)。由此,如果有char p[6]="Hello";这样的语句,编译器实际上做的工作是把Hello与' '一个个的读到p这个字符数组里。这是一个拷贝的工作,从只读段一个个的拷贝到char所在的数据存储段。所以在内存的不同区域其实有两份Hello,一份在rodata区,也就是字符串常量所在的地方,另一份取决于p定义的位置位于全局变量区或者局部变量去(栈)。
同样地,还有一个很重要的区别————
char p[]="Hello";与char*p="Hello";两种方式定义出来的p是不同的!第一个p是一个字符型数组,它的内容是从字符串常量段复制来的,所以它就是个朴实的char*;而第二个p是指向字符串常量的,因此它实际上是const char*!
※由此:对于变量的定义来说,定义在哪,也就是这个变量将会位于内存的哪个位置,是由操作系统来决定的;而变量的具体的大小,是由使用者,也就是程序员来决定的。
③typedef的一些补充知识:
※注意typedef永远跟着变量来看,而不是跟着类型来思考————也就是说,把typedef遮起来,后面的那块就成了一个变量的定义。那么在这个定义之下得出的对应的变量的类型,就是那个变量可以代表的类型!
拿一个例子来品:
int timez_t;//timez_t是int类型的变量 typedef int timez_t;//timez_t是int类型的别名
只有两个字的差别--因此在遮掉typedef,看出类型之后,把“变量”二字置换成“别名”,就知道代表的类型了。
※不过typedef更重要的作用其实是保证可读性
④接续函数指针:
1、函数指针的理解方式:
仍然从“回答问题”的角度来思考函数指针!
2、
四、C/C++的下标检查:以下这两段文字摘自:C/C++ 不检查数组下标是否越界 - Slyar Home
原来C/C++是不检查数组下标是否越界的?奇怪的事情。。。不检查下标是否越界可以有效提高程序运行的效率,因为如果你检查,那么编译器必须在生成的目标代码中加入额外的代码用于程序运行时检测下标是否越界,这就会导致程序的运行速度下降,所以为了程序的运行效率,C/C++才不检查下标是否越界。
自己写了一段检测程序测试这个问题,发现如果数组下标越界了,那么它会自动接着那块内存往后写。想了一下明白了,以前说不允许数组下标越界,并不是因为界外没有存储空间,而是因为界外的内容是未知的。也就是说如果界外的空间暂时没有被利用,那么我们可以占用那块内存,但是如果之前界外的内存已经存放了东西,那么我们越界过去就会覆盖那块内存,导致错误的产生
注意:下标引用,也就是[],是可以应用于任何指针的,也仅仅是相当于一个语法糖。
※中括号内的值指定的其实是偏移量,也就是相较于指针所在的当前位置,向内存前货后便宜多少个步长。
但是对于下标检查这个比较操作来说,需要程序中所有数组的位置和长度方面的信息,这将占用一些空间。当程序运行时,这些信息必须进行更新,以反映自动和动态分配的数组,这又将占用一定的时间。因此,即使是那些提供了下标检查的编译器通常也会提供一个开关,允许去掉下标检查。
我拿VS做了一下实验,发现Debug mode下是会进行比较严格的下标检查的,比如:
int main()
{
int a[5] = { 0 };
a[5] = 1;
}
这段码,文件后缀改.cpp或者.c,都会在编译生成的时候报错。
不过这种的“不那么明显的”操控越界内存的行为则不会报错:
int main()
{
int a[5] = { 0 };
int b = a[5];
}
但是!第一种的写法,切换到Release mode之后,就可以正常生成了,仅仅只会有一个警告。这一点也更加印证了编译器在Debug mode下确确实实会多做许多工作,而release mode更能保证程序的运行速度。
譬如这样,在release mode打上break point之后,会发现release mode把没用的东西全部扔掉了,也不会保存变量的临时状态,以至于我想这样观察变量的值都不可以。
五、字符串行为①补充--在字符中,' '代表的就是0,而不是'0'。并且字符中'xxx'的xxx会被视作八进制来处理!所以:二进制开头0b,十六进制开头0x,八进制开头0
②关于strlen函数--它的行为是不断地读取字符直到遇到' ',相当于从第一个字符开始,读到一个就长度++,直到遇到' ',返回长度。因此对于以下这个例子:
int main()
{
char str[]="Hello";
int a=strlen(str);
int b=sizeof(str)/sizeof(str[0]);
}
a的值是5,b的值是6,因为' '也在字符数组中算作一个字符,会被sizeof读入,而strlen不会读入' '。
③一个经典的统计特殊字符的案例:



