一篇关于c语言的大补帖
一晃今年又开始了,作为一个失意的中年技术男,现在的心境真的是五味杂陈。赶紧写一篇吧,我怕过了这个点,今年就在没有那个心情去写了。
因为是基础嘛,从事软件开发以来c或者c++相关的东西断断续续 也刷了差不多一遍。中间看的书差不多有 c++游戏编程入门教程,vc++深入详解, c++ primer plus ,c程序设计语言。
c语言和c++以及一些其它高级语言的区别
与其它高级语言 比如c# java 相比c++是一个开放式的开源的所谓的众人拾柴火焰高的解决方式,只规定了基本的语法和约定 各种其他,比如编译器都由各个厂商自己去实现 比如多线程没有 各种只要跟操作系统接口相关的 那么说白了还是不能跨平台的 所谓的跨平台只是说这个基本语法和格式约定 在各个平台编译大差不差。实际上这个众人拾柴还是火焰没高起来 。c++在服务器或者嵌入式 以及底层算法上用的是比较多的 ,但是Windows上快速的应用层开发,不如微软c#那种一揽子方案来得舒服。
然后说说c++与纯c,纯c里面并不支持string 也不支持include<string> 可以说是刀耕火种的地步了 ,std库不用说更是不支持。其实想想c++已经算是比较高级方便的了 。说归说 接触了c51 msp430 stm32 还是能够发现一些端倪的 ,从最初引导芯片进入首条指令执行的那几句汇编 为萌芽 到后面不断的喂指令 不断的循环 地址偏移 ,跳转 判断, 开启了 后面以软件为主题的这个学科 编程嘛 条件判断嘛 内存嘛 数据嘛 进程嘛 巴拉巴拉,像极了世界的一生二二生三三生万物,人类在芯片上运行程序进行流程控制 计算机最基础的雏形 关键之处 也在于此,汇编或者c语言正符合这种最初级的需求,你会发现原来这就是所谓的裸机编程 并没有操作系统上层一堆的东西参与也没有库,一切都是刀耕火种 ,不是说它多么的高深多么难 其实就是这么的纯粹而已。 基于它的这种直接内存控制的高效机制 所以做底层算法的 大多还是用C/C++这玩意儿。
对于初学者某些东西如果开始选择难的如果弄不出来是很容易消磨人的意志的 ,作为普通人谁不想拖拖鼠标就能做出来功能呢。还有所谓的动态内存分配,在c++里所谓的动态内存分配概念就是一个函数大括号下来里面运行的东西叫自动变量 放在栈上 ,括号结束即从栈上清除掉,动态内存就仅是指new出来的东西 方法运行结束了还没消失的东西,搞了单片机那一套后才发现 也就几百行的程序就算大程序了 并且也根本用不上所谓的动态内存分配 ,根据cpu的时钟频率 ,简单粗暴的的单线程循环 代表cpu的一个运转周期 ,恍然觉得最简单的编程原来可不就是这样吗,值处理和指针 以及位操作 有那么一点奇技淫巧存在。以前只知道有些使用位操作编写方式可提高运行效率,不觉得所以然,在资源局限的单片机上做了工作 以及对那种机制了解过后 就会明白为啥可以提高运行效率,以及逼着你必须那样去做。
一些基本上手
纯c嘛,万事当然从printf开始,来一段printf基本操作
1 void PrintTest(){ 2 int a = 23; 3 //打印一个整数 ,注意这里有陷阱哈 ,如果是一个浮点数35.1并不会输出35 而是0 4 printf("%d\n", a); 5 6 //纯c里面并不支持string 也不支持include<string> 7 //string str1("sdfdfdf33444"); 8 //纯c打印字符串 9 char * str1 = "nihao\0"; 10 printf("%s\n", str1); 11 //运算任意一侧有浮点数 运算之前整数操作会被转为浮点数 12 printf("%f\n", 5.0 / 9); 13 printf("%3.2f\n", 643.1415926); 14 //%02X 格式化为16进制 比如0x05 不足2位的前面会以0补齐 15 printf("%02X\n", 5); 16 printf("Hello world!\n"); 17 }
用惯了高级语言 新手老是说不就是输出个字符串吗 c怎么这么麻烦,咋个又不对了,所谓万事开头难,然后是人类编程流程控制最基础的 数字 文本转换了 ,还有文本的格式化 ,对不对 ,有木有。
1 void PrintTest2(){ 2 3 //纯c里面利用sprintf_s实现类似stringFormat的效果 4 //标准c里是支持sprintf的 ,vc里面不支持 使用sprintf_s作为替代 5 int a = 23; 6 char buff[8] = { 0 }; 7 sprintf_s(buff, "%d个\0", a); 8 printf("%s\n", buff); 9 10 //c语言中的字符串拼接 11 //str1+str2; c语言里并不能这样 12 //字符串数组方式 13 char str1[] = "hello ";//可以换成 char * str1= 14 char str2[] = "world";//可以换成 char * str2= 15 char str3[20] = "";//这个不能换 16 strcat_s(str3, str1);//strcat_s也是作为strcat的替代 17 strcat_s(str3, str2); 18 printf("%s\n", str3); 19 20 //数字转字符串 21 int num = 4568; 22 char number[15]; 23 _itoa_s(num, number, 10);//_itoa_s也是作为itoa的替代 24 printf("%s\n", number); 25 //字符串转数字 26 char * number2 = "8957"; 27 int number33; 28 number33=atoi(number2); 29 printf("parse Int :%d\n", number33); 30 31 //上面的两种都可以用下面的解决方案 32 //但是注意str5必须要先分配固定内存,也不能使用char 33 char * str4 = "hello\0"; 34 int num1 = 6749; 35 char str5[20]; 36 sprintf_s(str5, "%s - %d", str4, num1); 37 printf("%s", str5); 38 39 }
然后c语言的一些基础的,比如函数体内的变量 在函数结束前销毁称之为自动变量,基本的变量赋值 表达式运算条件判断 。函数每次运行互相独立的 ,这些不用多说了吧。 注意上面代码的注释vc里面的某些函数跟标准c有一些出入 ,但是区别不大。注意sprintf的使用可以实现一些灵活的格式化字符串效果。然后你会发现c都是最基础的字节和地址操作 ,深入思考后如果你愿意上面类似的字符转数字函数你可以自己写一个 可不就是每个字节ascii码比对然后转换成二进制位吗,高级语言字符串 是怎么封装来的 ,单片机里的指令 内存 数据 ,c太原始了 由不得 你不去思考 它的本质了。c里的数组 地址偏移 指针 几个东西总是左右游走 亦数组亦指针 模棱两可,深入理解后你就会发现指针 包括高级语言的.点操作符 本质就是地址偏移 数组可以理解为一组连续偏移的变量。有时候通过这种机制实现灵活的内存操作 以及利用一些奇技淫巧实现一些高效的位操作,这既是纯c被人诟病的地方,也是它优势的地方 看你怎么理解咯。总之一句话就是 根据内存地址进行内存数据操作。基于以上 所以c++很大一部分都是在讲动态内存分配,c很大一部分是在讲位操作和指针。上代码:
玩出花的指针:
1 void PointerVoidTest(){ 2 //指针也要确定数据类型 才能输出它本来的值 ,这里有一点技巧 void指针可以跟其它指针转换 3 int * pi1; 4 int nn1 = 762; 5 void * pv1; 6 pv1 = &nn1; 7 //void指针转换的正确例子 ,(int *) 仅仅时转换成了一个int指针 前面还要加* 才输出本来的数 8 printf("%d aa", *(int *)pv1); 9 } 10 void PointerTest(){ 11 //我们平常编程写的字符常量"aaa" 这种的 或者参数 也是一种预分配内存形式的指针 12 13 //指针地址也可以进行比较== 类似于意思看是不是同一个引用 14 15 //c语言中地址算术运算是一致且又规律的 讲指针数组 和地址的算术运算集成在一起是该语言的一大优点 16 //函数定义中 char*s 和char s[] 是等价的 17 18 int a1[3][5]; 19 int *b[10]; 20 //对于C语言的初学者来说,很容易混淆二维数组与指针数组之间的区别,比如上面例子中 21 //int a[3][5]; 是一种正规数组做法 22 //int *b[10] 是一个10元素的数组 每个元素是一个int指针,还必须初始化每个指针的指向 23 //才是一个二维数组 ,比第一种的优势是 每个元素可以指向长短不一的数组 比如字符串应用 24 //指针数组 25 char * mname[] = { "illegal month", "jan", "feb", "mar" }; 26 //二维数组 除第一维以外其他维必须指明长度 27 char mname2[][15] = { "illegal month", "jan", "feb", "mar" }; 28 29 int ar[] = { 4, 5, 7, 6 }; 30 int * pa; 31 pa = ar;//等同于 pa=&ar[0]; 32 printf("%d--\n", *(pa + 1));//等同于ar[1] 注意这个微秒的区别 地址偏移 33 //*(pa+i) 为对应数组下标i的值 34 printf("%d--\n", *(ar + 1));//同样等同于 ar[1] 35 printf("%d--\n", pa[1]); 36 //看指针和数组偏移量 指针玩出花儿来 37 //p++ 如果是int那么地址也会增加对应的int类型字节长度 p89页 38 //void* 一种通用指针 39 //指针是能够存放一个地址的一组存储单元 通常是4字节 40 //指针还要确认他指向什么 以便知道长度 和后续操作 int * 代表指向int的指针 41 //&是取地址符 ,取变量对象的地址 42 //并不是说用了指针就等于动态内存分配 maloc new 那些才算,其实单片机编程中很少动态内存分配 43 //一元运算符*是简介寻址或简介引用运算符 当他用于指针时 将访问指针所指向的对象 44 45 //c语言的类型 数组 声明的前缀的组合非常灵活 46 int x = 1, y = 2, z[10]; 47 int *ip; 48 ip = &x;//现在ip指向x 49 y = *ip;//现在y=1; 50 //ip=&z[0];//现在IP只想z[0] //传数组变量其实是一种变相的方式 51 (*ip)++;//圆括号是必须的 否则 对地址进行+1运算 52 printf("%d--\n", *ip); 53 54 z[0] = 5; 55 z[1] = 8; 56 ip = &z[0]; 57 *(ip++); 58 printf("%d--\n", *ip); 59 60 //把一个指针复制给另一个指针 61 //指针也是变量 就是一个地址 直接使用 而不必使用间接引用符& 62 int *iq; 63 iq = ip; 64 printf("%d--\n", *iq); 65 66 printf("%d--\n", iq);//指针变量保存的地址 可以发现跟printf("%d--\n", ip);是一样的 67 printf("%d--\n", &iq);//而这个变量本身又保存在那个地址? 68 69 printf("------------------------------\n"); 70 *(&iq) = 0;// 71 printf("%d++\n", z[1]); 72 printf("%d--\n", iq);//指针指向的地址变为0 73 //现在已经是一个野指针 出来的int是不知道是什么值的,严重时程序会崩溃 74 //printf("%d--\n", *iq); 75 printf("%d--\n", &iq);//指针变量本身的地址依然客观存在 76 }
高阶的指针数组 地址偏移量操作本质 以及void指针类型转换
接下来是一个二维数组和指针相结合操作的例子:
1 void reIniAr2(int *b[])//unsigned short*a[], 2 { 3 for (int i = 0; i<2; i++){ 4 for (int j = 0; j<2; j++){ 5 //b[i][j] = 666; 6 *(*(b + i) + j) = 444; 7 } 8 } 9 } 10 void arr2dTest(){ 11 //数组已经声明后 不用想这种歪门子再去整体赋值 ,无论咋个都是不行的,必须的老老实实一个一个重新赋值 12 //然后另外一个c的数组长度并不允许通过变量确定 必须通过define 或者常量确定, 13 //网上评论说c99编译可以 int alen=3;int ar[alen] 14 //fqTab=&fqTab2; 15 //指针方式的2维数组处理 16 int a1[] = { 786, 894 }; 17 int b1[] = { 377, 945 }; 18 //这种方式是都支持的 两个一维数组地址 结合成指针形式的二维数组 19 int * ar[] = { &a1[0], &b1[0] }; 20 //这种方式codeblocks只是不会报错 但是后续操作会有莫名问题 vs2013直接不支持 21 //int * ar[] = { {1,2}, {3,4} }; 22 //*(*(ar + 1) + 0) = 444; 23 reIniAr2(ar); 24 //ar[0][0] = 555; 既可以通过这种方式访问元素,也可以通过下面这种地址偏移方式访问 25 //但是下面这种理解起来需要花一番功夫 26 printf("%d--bb\n", *(*(ar + 0) + 0)); 27 //但是由这种过度呢 *号代表取指针指向的本来的东西 28 printf("%d--bb\n", *(ar + 0)[0]); 29 30 //当然下面才是一种简便 正常数组的用法,上面的不强求 31 int a2[2][2] = {0}; 32 a2[0][0] = 734; 33 a2[0][1] = 841; 34 printf("%d--b2\n", a2[0][0]); 35 printf("%d--b2\n", a2[0][1]); 36 }
然后是一个二维数组操作的示例,注意通过传递void类型的指针 ,我们却把当成 unsigned short类型进行解析 以及地址偏移的技巧i*2是因为short占2字节
1 void OffSetDataReplaceFqTab(int adstart, int adcount, char * fqSegment) 2 { 3 for (int i = 0; i < adcount; i++) 4 { 5 int offsetStart = adstart + i - 10; 6 // "/"取高位 "%"取低位 c语言的常规套路 这里的使用也是这个理念 7 int iOffset = offsetStart / 11;//行偏移 8 int jOffset = offsetStart % 11;//列偏移 9 //低索引是高位 10 //unsigned short beadd = (((char)(fqSegment[i * 2]))<<8) | (char)(fqSegment[i * 2+1]); 11 //这样也是对的 12 //以前没写对因为没有深刻理解fqSegment[i] 就是代表那个元素&fqSegment[i]才是地址 13 //(unsigned short *) 只是转换成了指针 前面还要再加* 14 //当然转换成usigned short的过程也是根据我们常见计算机系统来的也就是Intel的little字节序 15 //低索引是低位 高索引是高位 16 //数组相当于连续定义的指针 17 //基本理念上就跟嵌入式视频c语言地址赋值的那个地方一致 18 //unsigned short beadd = *(unsigned short *)&fqSegment[i*2]; 19 unsigned short beadd = *(unsigned short *)(fqSegment+(i*2)); 20 fqTab[iOffset][jOffset] = beadd; 21 } 22 } 23 24 char pia[10] = 25 { 26 0x01, 0x01, 27 0xff, 0xff, 28 0x01, 0x01, 29 0, 3, 30 0, 4 }; 31 OffSetDataReplaceFqTab(21, 5, &pia[0]);
然后是另一个void类型指针转换的例子
1 void PointerVoidTest(){ 2 //指针也要确定数据类型 才能输出它本来的值 ,这里有一点技巧 void指针可以跟其它指针转换 3 int * pi1; 4 int nn1 = 762; 5 void * pv1; 6 pv1 = &nn1; 7 //void指针转换的正确例子 ,(int *) 仅仅时转换成了一个int指针 前面还要加* 才输出本来的数 8 printf("%d aa", *(int *)pv1); 9 }
还有常见的对指定内存地址赋值:
1 void PointerTest2(){ 2 //常规指针操作 3 int num1 = 4331; 4 int *ip; 5 ip = &num1; 6 //指向的地址本身也是一个门牌编号可以用int表示 7 int add1 = (int)ip; 8 printf("%d--\n", *ip); 9 printf("%d--\n", ip); 10 printf("%d--\n", add1); 11 12 int *ip2; 13 //然后我们把这个门牌号反过来又变成一个指针 14 ip2 = (int*)add1; 15 //进而通过*ip 即可实现所谓的对指定地址进行赋值 16 //汇编里面这是基础操作,也是面试时经常喜欢考的一道c语言基础题 17 *ip2 = 9745; 18 printf("--------------------\n"); 19 printf("%d--\n", num1); 20 printf("%d--\n", *ip); 21 printf("%d--\n", ip); 22 printf("%d--\n", add1); 23 }
函数指针(又叫委托 或者回调方法 巴拉巴拉)
接下来是函数指针 的用法 ,我们最开始得弄两个参数一样的方法 作为同等级调用区分示例 对吧
1 //函数指针简单使用 概念类似c#的委托 2 int method1(int a){ 3 printf("method11:%d\n",a); 4 return 0; 5 } 6 7 int method2(int a){ 8 printf("method22:%d\n",a); 9 return 0; 10 }
然后是调用套路,普通的形参定义 void method1(int a) ,这里int a 就代表形参了。当参数是函数委托的时候 int(*delegate)(int ) 就成了函数指针, *号括号后面再括号 就认为是方法引用,不要问为什么 ,就这么不讲道理 姑且死记硬背吧。
1 void calback1(int(*delegate)(int ))//接受返回一个int的函数委托 2 { 3 int a = delegate(3); 4 printf("a=%d--\n", a); 5 } 6 calback1(method2);
手持表里面一种函数指针的用法
1 //定义委托 接受int形参 返回int的方法 2 typedef int (del)( int); 3 4 //委托数组 5 del *ds[] = { 6 method1, 7 method2 8 }; 9 //调用委托数组元素进行执行 10 //以前只知道数组元素可以通过索引取出,没听说过元素还能“执行”真是神奇 11 ds[0](555); 12 ds[1](777);
枚举和结构体
定义
1 //枚举统一声明方式 别忘了typedef 2 typedef enum 3 { 4 one, 5 two, 6 three, 7 } num; 8 9 //结构体的统一声明方式 //别忘了typedef 10 //typedef 类型定义 11 typedef struct 12 { 13 int x; 14 int y; 15 } pp; 16 //typedef int numb1; //typedef char*string ; 书上说标准库的string就是这么来的 17 //c语言的理念是一个处处能找到源头的东西的 ,只要你想深挖 18 //还有一些基本常识,='A'与=65 在编译器级别 是一样的没区别 19 //int cr ='A';//cr=65 20 21 //位字段 22 struct 23 { 24 unsigned int is_keyword : 1;//关键点就在于这个 :1 吗? 25 unsigned int is_extern : 1; 26 unsigned int is_static : 1; 27 } flags;
结构体调用测试,还有以前面试的时候被问题及sizeof 问题的时候还以为 c内部有什么封装 ,到头来发现也就是结构体内所有元素数据类型加起来的总长度而已,还有个新鲜的可能是以前没有接触过的那就是位字段操作,在单片机上使用很适合
1 void strAndEnumTest(){ 2 //枚举测试 3 num n1 = one; 4 num n2 = two; 5 6 if (n2 == two) 7 printf("ok--a\n"); 8 9 printf("%d--a\n", sizeof(pp));//pp结构体里有两个int 输出8 10 //结构体 ,指针的访问方式 ->箭头运算符 11 //struct pp p3={1,3}; 12 pp p2 = { 5, 8 }; //c的结构有点数组的味道,跟c#不一样 注意不要写成{ x=5,y=8} 13 //c语言和c++中的很多时候 初始化 没有new xxx 这样的,不像c# 14 //就是类指针和实例 的概念 习惯一下就好了 15 pp * p1; 16 p1 = &p2; 17 p1->x = 2;//等价 (*p1).x 18 19 //struct pp or1,*p3 //c的声明组合真的非常灵活 20 printf("%d--\n", p1->x); 21 printf("%d--\n", p2.x);//普通的直接的访问方式 22 printf("%d--\n", p2.y);// 23 printf("%d--a\n", sizeof(p2)); 24 25 26 //位字段操作 27 //果然是可以的 直接整体方便的方式进行位操作 28 //除了51单片机的基础例子有类似的方式 ,其他时候其实并没有见用过 29 flags.is_keyword = 0;//2进制的第2位 30 flags.is_extern = 1;//2进制的第1位 31 flags.is_static = 0;//2进制的第0位 32 printf("%d\n", flags);//还可以整体输出合并的值 33 }