开始本节学习笔记之前,先说几句题外话。其实对于C语言深度解剖这本书来说,看完了有一段时间了,一直没有时间来写这篇博客。正巧还刚刚看完了国嵌唐老师的C语言视频,觉得两者是异曲同工,所以就把两者一起记录下来。等更新完这七章的学习笔记,再打算粗略的看看剩下的一些C语言的书籍。
本节知识:
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- int main(void)
- {
- static int j=0;
- int k;
- void fun1()
- {
- j=0;
- j++;
- printf("fun1 %d\n",j);
- }
- void fun2()
- {
- static int i=0;
- //i=0;
- printf("fun2 %d\n",i);
- i++;
- }
- for(k=0;k<10;k++)
- {
- fun1();
- fun2();
- }
- return 1;
- }
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- void fun(int b[100])
- {
- printf("sizeof(b) is %d\n",sizeof(b));
- }
- int main(void)
- {
- int *p=NULL;
- int a[100];
- int b[100];
- printf("sizeof(p) is %d\n",sizeof(p));
- printf("sizeof(*p) is %d\n",sizeof(*p));
- printf("sizeof(a[100]) is %d\n",sizeof(a[100]));
- printf("sizeof(a) is %d\n",sizeof(a));
- printf("sizeof(&a) is %d\n",sizeof(&a));
- printf("sizeof(&a[0] is %d\n",sizeof(&a[0]));
- fun(b);
- return 1;
- }
6.关键字if:
a.对于bool类型的比较:FLASE都是0 TRUE不一定是1 所以应该用if(bool_num); if(!bool_num);
plaincopy
- cosnt int* func()
- {
- static int count = 0;
- count++;
- return &count;
- }
h.在看const修饰谁,谁不变的问题上,可以把类型去掉再看,代码如下:
plaincopy
- struct student
- {
- }*str;
- const str stu3;
- str const stu4;
str是一个类型 ,所以在去掉类型的时候,应该都变成const stu3和const stu4了,所以说应该是stu4和stu3这个指针不能被赋值。
12.关键字volatile:
volatile搞嵌入式的,一定都特别属性这个关键字,记得第一使用这个关键字的时候是在韦东山老师的,Arm裸机视频的时候。volatile是告诉编译不要对这个变量进行任何优化,直接在内存中进行取值。一般用在对寄存器进行赋值的时候,或修饰可能被多个线程访问的变量。
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- int main(void)
- {
- int a[5]={1,2,3,4,5};
- int *p=(int *)(&a+1); //数组指针 加一 进行正常的指针运算 走到数
- 组尾
- int *d=(int *)((int)a+1);//地址加一 不是指针运算
- //printf("%x\n",*((char *)((int)a+1)-1));
- /*因为是小端存储 高地址 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 低地址*/
- /*变成了 0x02 0x00 0x00 0x00 */
- printf("%x,%x",p[-1],*d); /* 第二个值就是这么存储的0x02 0x00 0x00 0x00 低地址处 所以就是2000000*/
- int a=0x11223344;
- char *p=(char *)((int)&a);
- printf("%x\n%x\n",*(p+0),p+0);
- printf("%x\n%x\n",*(p+1),p+1);
- return 0;
- }
下面是一个利用union判断PC是大端小端的例子,代码如下:
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- union
- {
- int i;
- char a[2];
- }*p,u;
- int main(void)
- {
- p=&u;
- p->i=0x3839;
- printf("%x\n",p->i);
- printf("a0p=%x,a1p=%x\n",&(p->a[0]),&(p->a[1]));
- printf("a0=%x,a1=%x\n",p->a[0],p->a[1]);
- return 0;
- }
15.enum关键字:
枚举enum其实就是int类型,用来保存枚举常量的。enum枚举类型,这个才是真正的常量,定义常量一般用enum 。#define是宏定义是在预编译期间单纯的替换。#define宏定义无法调试,枚举常量是可以调试的。#define宏定义是无类型信息的,枚举类型是有类型信息的常量,是int型的。
a.typedef用于给一个已经存在的数据类型重新命名。
plaincopy
- typedef unsigned int int32;
typedef char* PCHAR; PCHAR p1,p2; //p1和p2都是 char*型
e.有一个知识点忘记了,嘿嘿,程序如下:
plaincopy
- typedef struct student
- {
- }str,*str1;
str1 abc; 就是定义一个struct student *类型
str abc; 就是定义一个struct student 类型
plaincopy
- 程序一:
- for(i=0; i<m; i++)
- {
- for(j=0; j<n; j++)
- {
- for(k=0; k<p; k++)
- {
- c[i][j] = a[i][k] * b[k][j];
- }
- }
- }
- 程序二:
- for(i=0; i<m; i++)
- {
- for(k=0; k<p; k++)
- {
- for(j=0; j<n; j++)
- {
- c[i][j] = a[i][k] * b[k][j];
- }
- }
- }
。所以说第二种写法效率高。
1000 0001 ,取低八位是1000 0001,所以打印的应该是-127。
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- int main()
- {
- char a[1000];
- int i;
- for(i=0; i<1000; i++)
- {
- a[i] = (-1-i);
- }
- while(a[i])
- {
- printf("%d\n",a[i]);
- i++;
- }
- printf("%d\n",strlen(a));
- return 0;
- }
本节遗留问题:
本节接触了,C语言中的三大蛋疼:符号优先级 ++i顺序点 贪心法 (其实这里面好多都是跟编译器有关的,而且有好多问题都是可以通过良好的编程习惯避免的)
本节知识点:
1.注释问题:
注释不能把关键字弄断,如:in/*注释*/t
注释不是简单的剔除,而是使用空格替换
编译器认为双引号括起来的内容都是字符串,双斜杠也不例外。如:char *p = "heh//jfeafe" //不起注释作用
2.接续符:
接续符\ ,常用于宏定义中
plaincopy
- #define SWAP(a,b) \
- { \
- int temp = a; \
- a = b; \
- b = temp; \
- }
反斜杠同时有接续符和转义符两个用途,当接续符使用的时候,可以直接在程序中出现。当转义符使用的时候,必须是出现在字符串中。
接续符,也用与接续一个关键字,代码如下, 注意: 但是直接连接\两边不能有空格。
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- cha\
- r a = 12;
- return 0;
- }
3.逻辑运算符:有一个短路规则
4.最容易忘记规则的两个运算符:
三目运算符:(a?b:c) 当a的值为真的时候 返回b的值,否则返回c的值
逗号表达式:a,b 表达式的值为b的值
5.位运算:
对于左移和右移<< >>问题 :无符号的,和有符号左移,都是补0 ,对于有符号的在右移动的时候,正数补零,负数补什么跟编译器有关系。并且左移和右移的大小不能大于数据的长度,也不能小于0。
交换两个数,有一种不借助中间变量的方法,就是异或,代码如下:
plaincopy
- #include <stdio.h>
- #define SWAP1(a,b) \
- { \
- int temp = a; \
- a = b; \
- b = temp; \
- }
- #define SWAP2(a,b) \
- { \
- a = a + b; \
- b = a - b; \
- a = a - b; \
- }
- #define SWAP3(a,b) \
- { \
- a = a ^ b; \
- b = a ^ b; \
- a = a ^ b; \
- }
- int main()
- {
- int a = 1;
- int b = 2;
- SWAP1(a,b);
- SWAP2(a,b);
- SWAP3(a,b);
- return 0;
- }
6.i++,i--顺序点:
只有 i++ i--才有顺序点 就是什么时候开始加,什么时候开始减。真心对于顺序点 是搞不懂啊~~~ (++i)+(++i)+(++i) ,在gcc中是5+5+6(DEV C++) ,在vc中是6+6+6(vc++6.0) ,不同编译器顺序点不一样。这个例子的顺序点 在; 前。
a=((++i),(++i),(++i)) 它的顺序点在每个逗号前面完成计算。我觉得特殊的顺序点 是可以通过合理的顺序布局来避免的。
7.贪心法:
每一个符号应该尽可能多的包含字符
8.符号运算优先级问题:
个人觉得优先级不用记,好好的写括号吧~~~
给一个易错优先级表,如图:
9.c语言中的类型转换:
c语言中有两种转换类型,分别是:隐式转换和显示转换(强制类型转换)
隐式转换的规则:
a.算术运算中,低类型转换为高类型
b.赋值运算中,表达式的类型转换为左边变量的类型
c.函数调用时,实参转换成形参的类型
d.函数返回值,return表达式转换为返回值的类型
隐式转换的例子,代码如下:
plaincopy
- #include <stdio.h>
- int main()
- {
- int i = -2;
- unsigned int j = 1;
- if( (i + j) >= 0 )
- {
- printf("i+j>=0\n");
- }
- else
- {
- printf("i+j<0\n");
- }
- printf("i+j=%d\n", i + j);
- return 0;
- }
注意:在使用C语言的时候,应该特别注意数据的类型是否相同,尽量避免隐式转换带来的不必要的麻烦~~~
三、
本节知识点:
1.编译过程的简介:
预编译:
a.处理所有的注释,以空格代替。
b.将所以#define删除,并展开所有的宏定义,字符串替换。
c.处理条件编译指令#if,#ifdef,#elif,#else,#endif
d.处理#include,并展开被包含的文件,把头文件中的声明,全部拷贝到文件中。
e.保留编译器需要使用的#pragma指令、
怎么样观察这些变化呢?最好的方法就是在GCC中,输入预处理指令,可以看看不同文件经过预处理后变成什么样了,预处理指令:gcc -E file.c -o file.i 注意:-C -E一起使用是预编译的时候保留注释。
编译:
a.对预处理文件进行一系列词法分析,语法分析和语义分析
词法分析:主要分析关键字,标示符,立即数等是否合法
语法分析:主要分析表达式是否遵循语法规则
语义分析:在语法分析的基础上进一步分析表达式是否合法
b.分析结束后进行代码优化生成相应的汇编代码文件 编译指令:gcc -S file.c -o file.s
汇编:
汇编器将汇编代码转变为机器可以执行的指令,每个汇编语句几乎都对应一条机器指令,其实机器指令就是机器码,就是2进制码。汇编指令:gcc -c file.c -o file.o 注意:-c是编译汇编不连接。
链接:
再把产生的.o文件,进行链接就可以生成可执行文件。连接指令:gcc file.o file1.o -o file 这句指令是链接file.o和file1.o两个编译并汇编的文件,并生成可执行文件file。
链接分两种:静态链接和动态链接,静态链接是在编译器完成的,动态链接是在运行期完成的。静态链接的指令是:gcc -static file.c -o file对于一些没有动态库的嵌入式系统,这是常用的。
一般要想通过一条指令生成可执行文件的指令是: gcc file.c -o file
资料:这里面说到了很多关于gcc的使用的问题,我提供一个gcc的学习资料,个人觉得还不错,也不长,就是一个txt文档,很全面。资源下载地址http://download.csdn.net/detail/qq418674358/6041183
Ps:嘿嘿,设了一个下载积分,因为真的是没分用了!希望大家见谅哈!
2.c语言中的预处理指令:#define、#undef(撤销已定义过的宏名)、#include、#if、#else、#elif、#endif、#ifdef、#ifndef、#line、#error、#pragma。还有一些ANSI标准C定义的宏:__LINE__、__FILE__、__DATA__、__TIME__、__STDC__。这样使用printf("%s\n",__TIME__); printf(__DATE__);
一个#undef的例子:
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #define X 2
- #define Y X*2
- #undef X
- #define X 3
- int main()
- {
- printf("%d\n",Y);
- return 0;
- }
这个输出的是6,说明了#undef的作用
3.宏定义字符串的时候:应该是 #define HELLO "hello world" 记住是双引号。还有就是一切宏都是不能有分号的,这个一定要切忌!!!
4.宏与函数的比较:
a.宏表达式在预编译期被处理,编译器不知道有宏表达式存在
b.宏表达式没有任何的"调用"开销
c.宏表达式中不能出现递归定义
5.为什么不在头文件中定义全局变量:
如果一个全局变量,想要在两个文件中,同时使用,那这两个文件中都应该#include这个头文件,这样的话就会出现重复定义的问题。其实是重名的问题,因为#include是分别在两个文件中展开的,试想一下,如果在两个文件中的开始部分,都写上int a = 10; 是不是也会报错。可能你会说那个#ifndef不是防止重复定义吗?是的 ,那是防止在同一个文件中,同时出现两次这个头文件。现在是两个文件中,所以都要展开的。全局变量就重名了!!!所以 对于全局变量,最好是定义在.c文件中,不要定义在头文件中。
6.#pargma pack 设置字符对齐,看后面一节专门写字符对齐问题的!!!
7.#运算符(转换成字符串):
假如你希望在字符串中包含宏参数,那我们就用#号,它把语言符号转换成字符串。
#define SQR(x) printf("the "#x"lait %d\n",((x)*(x)));
SQR(8)
输出结果是:the 8 lait 64 这个#号必须使用在带参宏中
有个小例子:
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- /*在字符串中 加入宏参用的*/
- #define SCAN(N,String) scanf("%"#N"s",String); //N是截取的个数 String是存储的字符串
- int main()
- {
- char dd[256];
- SCAN(3,dd) //记得没有分号哈 自定义 任意格式输入的scanf 截取输入的前三个
- printf("%s\n",dd);
- return 1;
- }
8.##运算符(粘合剂)
一般用于粘贴两个东西,一般是用作在给变量或函数命名的时候使用。如#define XNAME(n) x##n
XNAME(8)为8n 这个##号可以使用在带参宏或无参宏中
下面是一个##运算符的小例子,代码如下:
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #define BL1 bb##ll##1
- #define BL(N) bbll##N
- int main()
- {
- int BL1=10;
- int BL(4)=15;
- printf("%d\n",bbll1);
- printf("%d\n",bbll4);
- return 1;
- }
注意:#号和##号都必须只能在宏定义中使用,不能使用在其他地方
9.其实预编译这块还有一些,不常用到的预编译指令,也是盲点,但是不难理解,用到的时候查查就好。比如说#line、#error、#warning等。
很多人都觉得内存对齐这个问题很难,很不好算,总算错,其实我想说只要你画一画就没那么难了。好了,进入正题。
本节知识点:
1.结构体为什么要内存对齐(也叫字节对齐):
其实我们都知道,结构体只是一些数据的集合,它本身什么都没有。我们所谓的结构体地址,其实就是结构体第一个元素的地址。这样,如果结构体各个元素之间不存在内存对齐问题,他们都挨着排放的。对于32位机,32位编译器(这是目前常见的环境,其他环境也会有内存对齐问题),就很可能操作一个问题,就是当你想要去访问结构体中的一个数据的时候,需要你操作两次数据总线,因为这个数据卡在中间,如图:
在上图中,对于第2个short数据进行访问的时候,在32位机器上就要操作两次数据总线。这样会非常影响数据读写的效率,所以就引入了内存对齐的问题。
另外一层不太重要的原因是:某些硬件平台只能从规定的地址处取某些特定类型的数据,否则会抛出硬件异常。
2.内存对齐的规则:
a.第一个成员起始于0偏移处
b.每个成员按其类型大小和指定对齐参数n中较小的一个进行对齐
c.结构体总长度必须为所有对齐参数的整数倍
d.对于数组,可以拆开看做n个数组元素
3.来几个小例子,画画图,有助于理解:
第一个例子,代码如下:
plaincopy
- #include <stdio.h>
- struct _tag_str1
- {
- char a;
- int b;
- short c;
- }str1;
- struct _tag_str2
- {
- char a;
- short c;
- int b;
- }str2;
- int main()
- {
- printf("sizeof str1 %d\n",sizeof(str1));
- printf("sizeof str2 %d\n",sizeof(str2));
- return 0;
- }
输出的结果分别是:str1为12 str2为8,分析的过程如下图:
看图很自然就知道了str1为12个字节,str2为8个字节。
第二个例子,上面的那个例子有好多问题还没有考虑到,比如说上面的那个例子在8字节对齐,和4字节对齐的情况都是一样的。结构体中嵌套结构体的内存对齐怎么算,所以就有了这个例子,代码如下:
plaincopy
- #include <stdio.h>
- #pragma pack(8)
- //#pragma pack(4)
- struct S1
- {
- short a;
- long b;
- };
- struct S2
- {
- char c;
- struct S1 d;
- double e;
- };
- #pragma pack()
- int main()
- {
- struct S2 s2;
- printf("%d\n", sizeof(struct S1));
- printf("%d\n", sizeof(struct S2));
- printf("%d\n", (int)&(s2.d) - (int)&(s2.c));
- return 0;
- }
在Dev c++中,默认的是8字节对齐。我们分析下在4字节对齐的情况下输出的是,S2是20,S1是8,分析如图:
在4字节对齐的情况中,有一个问题值得注意:就是图中画1的地方。这里面本应short是可以上去的。但是对于结构体中的结构体一定要十分警惕,S1是一体的,short已经由于long进行了内存对齐,后面还空了两个字节的内存,其实此时的short已经变成了4个字节了!!!即结构体不可拆,不管是多少字节对齐,他们都是一体的。所有的圈都变成了叉。所以说结构体只能往前篡位置,不能改变整体。
我们在分析一些8字节对齐的情况,如图:
同样,到这里又有一个字节对齐的原则要好好重申一下:就是以什么为对齐参数,首先我们要知道编译器或者自己定义的是多少字节对齐的,这个数为n。然后我们要看这个结构体中的各个数据类型,找到所占字节数最大的类型,为m。如果n大于m,就以m为对齐参数,比如说一个4字节对齐的结构体中都是short,那这个结构体以什么为对齐参数,当然是2了,如果m大于n,就以n为对齐参数,比如说在4字节对齐的情况下的double类型。
以上就是我对内存对齐的小总结,最最想要说明的就是两大段红色的部分。
1.int a=9,b=10,d=9;是可以的。
2.%*d ,在scanf中使用的时候,是1整数但不赋给任何变量,有个小代码:
plaincopy
- #include <stdio.h>
- #include <malloc.h>
- int main()
- {
- int a=23,b=5,c=9;
- scanf("%*d%d%d",&a,&b,&c);
- printf("%d,%d,%d",a,b,c);
- return 0;
- }
a的值,你是赋值不进去的,仅仅占位用的。
3.对于冒泡排序,怎么在不完全执行完循环前就预先判断,已经排序结束了:
在一次内层循环的时候,一次都没有进行数据交换,就说名冒泡排序已经排序ok了。
4.不要总记得scanf,同样还存在getchar()和gets()函数,gets能接收含有空格的字符串,这个是scanf不能做到的。
scanf("%ls",a); //接收有效字符串的第一个字符
scanf("%ns",a); //这个是格式化输入,接收字符串的从头开始的n个字符
其实我想说,scanf函数真心没有什么用,很不好的一个函数。
5.堆区分配内存是从两头开始增长的,不是单向增长的。
6.typedef int [10] 其实[10]就是int了,个人觉得这个代码风格,很不好,千万不能写成这样,可读性很差!
7.要记住函数在传递参数的时候,其实是数据的拷贝,直接对形参进行改变或者赋值,是毫无意义的,实参是不会改变的。对于指针也是一样的。只有通过指针,取得了当前这个指针指向的内容的时候,改变了这个内容,这样实参才会被改变。因为是直接改变了内存地址中保存的数值。
举个例子就是:在数据结构那节中的链表,creat函数就是一个典型的例子。仔细想想为什么不能在main函数中定义一个头结点,再把这个头结点的地址传给creat函数呢?一定要通过creat返回一个头结点指针呢?再想想,为什么在想通过形参获得子函数中数据的时候,一定要传入地址或者指针呢?然后再把想要获得数据,写入这个地址或者指针中去?
给一段代码,帮助理解这个问题:
plaincopy
- #include <stdio.h>
- #include <malloc.h>
- typedef struct _tag_str
- {
- int a;
- int b;
- }str;
- void fun(str* str1)
- {
- str1 = (str* )malloc(sizeof(str));
- str1->a = 12;
- str1->b = 34;
- }
- int main()
- {
- /*str* strp;
- fun(strp);
- printf("%d\n",strp->a);
- printf("%d\n",strp->b);*/
- str str1;
- fun(&str1);
- printf("%d\n",str1.a);
- printf("%d\n",str1.b);
- }
想想,为什么子函数中赋值,在main中打印出来是不一样的!!!
对于fun(strp)的过程是这样的:在函数传递参数的时候,strp的值 赋值给了子函数的str1,这个过程就是函数参数拷贝的过程,然后str1的值在malloc的时候不幸被malloc改变了,所以在main中打印出来的不一样。
对于fun(&str1)的过程是这样的:在函数传递参数的时候,&str1的值 赋值给了子函数的str1,后面的过程跟上面一样。所以在main中打印的也是不一样的。
对于这种情况,最好的解决办法就是利用函数返回值,把str1返回 回来就ok了!!!
注意:可能你会问了,那怎样通过参数获得子函数传递的值啊,其实很简单,你在main中开辟好一段内存,然后把这个内存地址传递到子函数中去,然后对这个内存进行赋值,不要去改变这个指针的指向(即指针的值),仅仅改变指针指向的内存(即指针指向的内容),自然就获得了你想要的值!
8.c语言文件操作的一个问题:
c语言中打开文件有两种方式,一种是二进制方式,另一种是文本方式(ASCII码方式)。这两种方式有什么区别?(对于Linux这种只有一种文件类型的操作系统来说是没有区别的)
我们就以windows为例说说区别:
a.以文本方式打开文件,若将数据写入文件,如果遇到换行符'\n'(ASII 值为10,0A),则会转换为回车—换行'\r\n'(ASCII值为13,10,0D0A)存入到文件中,同样读取的时候,若遇到回车—换行,即连续的ASCII值13,10,则自动转换为换行符。
而以二进制方式打开文件时,不会进行这样的处理。
b.还有如果以文本方式打开文件时,若读取到ASCII码为26(^Z)的字符即0x1a,则停止对文件的读取,会默认为文件已结束,而以二进制方式读取时不会发生这样的情况。由于正常情况下我们手动编辑完成的文件是不可能出现ASCII码为26的字符,所以可以用feof函数去检测文件是否结束。
所以,由于存在上面的两个区别,我们在明确文件类型的时候,最好使用相对应的方式对文件进行打开。对于那些不明确文件类型的时候,最好使用二进制方式打开文件。
指针这一节是本书中最难的一节,尤其是二级指针和二维数组直接的关系。
本节知识点:
第二个意义是 数组名 sizeof(a) 为整体数组有多少个字节
int)p + sizeof(int)*1。对于什么typedef的,struct的,数组的都是一样的。
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- int main(int argc, char *argv[])
- {
- /* int a[20]={1,2,4};
- printf("%d\n",sizeof(a));
- printf("%p\n",a);
- printf("%p\n",&a);
- printf("%p\n",&a[0]);
- */
- /* int a[5]={1,2,3,4,5};
- int (*p)[5]=&a;
- printf("%d\n",*((int *)(p+1)-1));
- */
- int a[5]={1,2,3,4,5};
- int* p=(int *)(&a+1);
- // int *p=&a+1; //这个条语句是 把&a这个数组指针 进行了指针运算后 的那个地址 强制类型转换成了 int *指针
- printf("%d\n",*(p-1));
- return 0;
- }
5.访问指针和访问数组的两种方式:
数组是数组,指针是指针,根本就是两个完全不一样的东西。当然要是在宏观的内存角度看,那一段相同类型的连续空间,可以说的上是数组。但是你可以尝试下,定义一个指针,在其他地方把他声明成数组,看看编译器会不会把两者混为一谈,反过来也不会。
plaincopy
- char a[5]={'a','b','c','d','e'};
- char (*p)[3]=&a;
上面的代码是错误的,为什么?因为数组指针和数组不是一个类型,数组指针是指向一个数组元素为char 长度为3的类型的数组的,而这个数组的类型是数组元素是char长度是5,类型不匹配,所以是错的。
plaincopy
- #include <stdio.h>
- #include <assert.h>
- int strlen(const char* s)
- {
- return ( assert(s), (*s ? (strlen(s+1) + 1) : 0) );
- }
- int main()
- {
- printf("%d\n", strlen( NULL));
- return 0;
- }
d.自己动手实现strcpy,代码如下:
plaincopy
- #include <stdio.h>
- #include <assert.h>
- char* strcpy(char* dst, const char* src)
- {
- char* ret = dst;
- assert(dst && src);
- while( (*dst++ = *src++) != '\0' );
- return ret;
- }
- int main()
- {
- char dst[20];
- printf("%s\n", strcpy(dst, "hello!"));
- return 0;
- }
e.推荐使用strncpy、strncat、strncmp这类长度受限的函数(这些函数还能在字符串后面自动补充'\0'),不太推荐使用strcpy、strcmpy、strcat等长度不受限仅仅依赖于'\0'进行操作的一系列函数,安全性较低。
f.补充问题,为什么对于字符串char a[256] = "hello";,在printf和scanf函数中,使用a行,使用&a也行?代码如下:
plaincopy
- #include <stdio.h>
- int main()
- {
- char* p ="phello";
- char a[256] = "aworld";
- char b[25] = {'b','b','c','d'};
- char (*q)[256]=&a;
- printf("%p\n",a); //0022fe48
- //printf("%p\n",&a);
- //printf("%p\n",&a[0]);
- printf("tian %s\n",(0x22fe48));
- printf("%s\n",q); //q就是&a
- printf("%s\n",*q); //q就是a
- printf("%s\n",p);
- printf("%s\n",a);
- printf("%s\n",&a);
- printf("%s\n",&a[0]);
- printf("%s\n",b);
- printf("%s\n",&b);
- printf("%s\n",&b[0]);
- }
指针的地址,只有p是保存的hello字符串的首地址,所以只有p可以,其他都不可以。scanf同理,因为&a和a的值相同,且都是数组地址。
a是&a[0] a[0]是&a[0][0] a就是&&a[0][0] 然后再弄个2级指针出来,自己就蒙了!!!这是一个典型的错误,首先&&a[0][0]就没有任何意义,跟2级指针一点关系都没有,然后a[0]此时不代表数组首元素地址,所以这个等价是不成立的。Ps:一定要搞清概念,很重要!!! ),a[0]作为数组首元素地址的时候等价于&a[0][0]。但是二维数组的数组头有很多讲究,就是a(二维数组名)、&a(二维数组的数组地址)、&a[0](二维数组首元素地址 即a[0]一维数组的数组地址 a有的时候也表示这个意思)、a[0](二维数组的第一个元素
即a[0]一维数组的数组名)、&a[0][0](a[0]一维数组的数组首元素的地址 a[0]有的时候也表示这个意思),这些值都是相等,但是他们类型不相同,行为也就不相同,意义也不相同。分析他们一定要先搞清,他们分别代表什么。
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- int main(int argc, char *argv[])
- {
- int a[3][3]={1,2,3,4,5,6,7,8,9};
- printf("%d\n",sizeof(a[0]));
- printf("%d\n",*a[2]);
- printf("%d\n",*(a[0]+1));
- printf("%p\n",a[0]);
- printf("%p\n",a[1]);
- printf("%p\n",&a[0]+1); //&a[0]+1 跟 a[1]不一样 指针类型不一样 &a[0]+1这个是数组指针 a[1]是&a[1][0] 是int*指针
- printf("%d\n",*((int *)(&a[0]+1)));
- printf("%d\n",*(a[1]+1));
- printf("%p\n",a);
- printf("%p\n",&a);
- printf("%p\n",&a[0]);
- printf("%d\n",sizeof(a)); //这是a当作数组名的时候
- printf("%d\n",*((int *)(a+1))); //此时 a是数组首元素的地址 数组首元素是a[0]
- //首元素地址是&a[0] 恰巧a[0]是数组名 &a[0]就变成了数组指针
- return 0;
- }
总结:对于a和a[0]、a[1]等这些即当作数组名,又当作数组首元素地址,有时候还当作数组元素(即使当作数组元素,也无非就是当数组名,当数组首元素地址两种),这种特殊的变量,一定要先搞清它现在是当作什么用的。
plaincopy
- int *q;
- q = (int *)a;
- printf("%d\n",*(q+6));
plaincopy
- int (*p)[3];
- p = a;
- printf("%d\n",*(*(p+1)+1));
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- int main()
- {
- int a[3][3]={1,2,3,4,5,6,7,8,9};
- int (*p)[3];
- int *q;
- printf("%d\n",*(*(a+1)+1)); //a *(&a[0]+1)
- p = a;
- q = (int *)a;
- printf("%d\n",*(*(p+1)+1));
- printf("%d\n",*(a[1]+1));
- printf("%d\n",a[1][1]);
- printf("%d\n",*(q+6));
- }
- <span style="font-family:Arial;BACKGROUND-COLOR: #ffffff"></span>
plaincopy
- <span style="color:#000000;">#include <stdio.h>
- int main()
- {
- int a[5][5];
- int(*p)[4];
- p = a;
- printf("%d\n", &p[4][2] - &a[4][2]);
- }</span>
plaincopy
- #include <stdio.h>
- #include <malloc.h>
- int reset(char**p, int size, int new_size)
- {
- int ret = 1;
- int i = 0;
- int len = 0;
- char* pt = NULL;
- char* tmp = NULL;
- char* pp = *p;
- if( (p != NULL) && (new_size > 0) )
- {
- pt = (char*)malloc(new_size);
- tmp = pt;
- len = (size < new_size) ? size : new_size;
- for(i=0; i<len; i++)
- {
- *tmp++ = *pp++;
- }
- free(*p);
- *p = pt;
- }
- else
- {
- ret = 0;
- }
- return ret;
- }
- int main()
- {
- char* p = (char*)malloc(5);
- printf("%0X\n", p);
- if( reset(&p, 5, 3) )
- {
- printf("%0X\n", p);
- }
- return 0;
- }
(2)函数中传递指针数组的时候,实参(指针数组)要退化成形参(二级指针)。
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- int main(int argc, char *argv[])
- {
- char* p[4]={"afje","bab","ewrw"};
- char* *d=p;
- printf("%s\n",*(p+1));
- printf("%s\n",*(d+1)); //d &p[0] p[0]是"afje"的地址,所以&p[0]是保存"afje"字符串的char*指针的地址
- return 0;
- }
d.子函数malloc,主函数free,这是可以的(有两种办法,第一种是利用return 把malloc的地址返回。第二种是利用二级指针,传递一个指针的地址,然后把malloc的地址保存出来)。记住不管函数参数是,指针还是数组, 当改变了指针的指向的时候,就会出问题,因为子函数中的指针就跟主函数的指针不一样了,他只是一个复制品,但可以改变指针指向的内容。这个知识点可以看<在某培训机构的听课笔记>这篇文章。
13.数组作为函数参数:数组作为函数的实参的时候,往往会退化成数组元素类型的指针。如:int a[5],会退化成int* ;指针数组会退化成二级指针;二维数组会退化成一维数组指针;三维数组会退化成二维数组指针(三维数组的这个是我猜得,如果说错了,希望大家帮我指出来,谢谢)。如图:
二维数组作为实参的例子:
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- int fun(int (*b)[3]) //此时的b为 &a[0]
- {
- printf("%d\n",*(*(b+1)+0));
- printf("%d\n",b[2][2]);// b[2][2] 就是 (*(*(b+2)+2))
- printf("%d\n",*(b[1]+2));
- }
- int main(int argc, char *argv[])
- {
- int a[3][3]={1,2,3,4,5,6,7,8,9};
- fun(a);//与下句话等价
- fun(&a[0]);
- return 0;
- }
数组当作实参的时候,会退化成指针。指针当做实参的时候,就是单纯的拷贝了!
14.函数指针与指针函数:
a.对于函数名来说,它是函数的入口,其实函数的入口就是一个地址,这个函数名也就是这个地址。这一点用汇编语言的思想很容易理解。下面一段代码说明函数名其实就是一个地址,代码如下:
plaincopy
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdbool.h>
- void abc()
- {
- printf("hello fun\n");
- }
- int main(int argc, char *argv[])
- {
- void (*d)();
- void (*p)();
- p = abc;
- abc();
- printf("%p\n",abc);
- printf("%p\n",&abc);//函数abc的地址0x40138c
- p();
- (*p)();
- d = ((unsigned int*)0x40138c); //其实就算d= 0x40138c这么给赋值也没问题
- d();
- return 0;
- }
可见函数名就是一个地址,所以函数名abc与&abc没有区别,所以p和*p也没有区别。
b.我觉得函数指针最重要的是它的应用环境,如回调函数(其实就是利用函数指针,把函数当作参数进行传递)代码如下,还有中断处理函数(同理)详细见<
ok6410学习笔记(16.按键中断控制led)>中的 中断注册函数,request_irq。还有就是函数指针数组,第一次见到函数指针数组是在zigbee协议栈中。
回调函数原理代码:
plaincopy
- #include <stdio.h>
- typedef int(*FUNCTION)(int);
- int g(int n, FUNCTION f)
- {
- int i = 0;
- int ret = 0;
- for(i=1; i<=n; i++)
- {
- ret += i*f(i);
- }
- return ret;
- }
- int f1(int x)
- {
- return x + 1;
- }
- int f2(int x)
- {
- return 2*x - 1;
- }
- int f3(int x)
- {
- return -x;
- }
- int main()
- {
- printf("x * f1(x): %d\n", g(3, f1));
- printf("x * f2(x): %d\n", g(3, &f2));
- printf("x * f3(x): %d\n", g(3, f3));
- }
注意:可以使用函数名f2,函数名取地址&f2都可以,但是不能有括号。
c.所谓指针函数其实真的没什么好说的,就是一个返回值为指针的函数而已。
15.赋值指针的阅读:
a.char* (*p[3])(char* d); 这是定义一个函数指针数组,一个数组,数组元素都是指针,这个指针是指向函数的,什么样的函数参数为char* 返回值为char*的函数。
分析过程:char (*p)[3] 这是一个数组指针、char* p[3] 这是一个指针数组 char* 是数组元素类型、char* p(char* d) 这个是一个函数返回值类型是char* 、char (*p)(char* d)这个是一个 函数指针。可见char* (*p[3])(char* d)是一个数组 数组中元素类型是 指向函数的指针,char* (* )(char* d) 这是函数指针类型,char* (* )(char* d) p[3] 函数指针数组 这个不好看 就放里面了。(PS:这个看看就好了~~~当娱乐吧)
b.函数指针数组的指针:char* (*(*pf)[3])(char* p) //这个就看看吧 我觉得意义也不大 因为这个逻辑要是一直下去 就递归循环了。
分析过程:char* (* )(char *p) 函数指针类型,char* (*)(char *p) (*p)[3] 函数指针 数组指针 也不好看 就放里面了。
本节知识点:
Ps:实际中进程使用的是esp和ebp两个指针,这里仅仅用sp是想说明那个意思而已! 操作系统使用的栈空间就必须符合sp指针的压栈和出栈方式,也就是遵循了cpu决定的栈的生长方式。编译器要想编译出能在这个操作系统平台上使用的程序,也必须要遵守这个规则,所以来看这个栈的生长方式是由cpu决定的。这也是为什么我用那么长的篇幅来解释sp指针是怎么工作的原因!
- #include <stdio.h>
- void fun()
- {
- int a;
- int b;
- int c;
- printf("funa %p\n",&a);
- printf("funb %p\n",&b);
- printf("func %p\n",&c);
- }
- void main()
- {
- int a;
- int b;
- int c;
- int d;
- int e;
- int f;
- int p[100];
- printf("a %p\n",&a);
- printf("b %p\n",&b);
- printf("c %p\n",&c);
- printf("d %p\n",&d);
- printf("e %p\n",&e);
- printf("f %p\n",&f);
- printf("p0 %p\n",&p[0]);
- printf("p1 %p\n",&p[1]);
- printf("p2 %p\n",&p[2]);
- printf("p3 %p\n",&p[3]);
- printf("p4 %p\n",&p[4]);
- printf("p10 %p\n",&p[10]);
- printf("p20 %p\n",&p[20]);
- printf("p30 %p\n",&p[30]);
- printf("p80 %p\n",&p[80]);
- printf("p90 %p\n",&p[90]);
- printf("p100 %p\n",&p[100]);
- fun();
- }
运行结果如下(不同编译器的运行结果是不一样的):
通过上面的运行结果,可以分析得出:在同一个函数中,先定义的变量在高地址处,后定义的变量在低地址处,且他们的地址是相连的中间没有空隙。定义的数组是下标大的在高地址处,下标小的在低地址处(由此可以推断出malloc开辟出的推空间,也应该是下标大的在高地址处,下标小的在低地址处)。子函数中的变量,跟父函数中的变量的地址之间有很大的一块空间,这块空间应该是两个函数的其他活动记录,且父函数中变量在高地址处,子函数中的变量在低地址处。
- #include<stdio.h>
- /*这是一个死循环*/
- /*这里面有数组越界的问题*/
- /*有栈空间分配的问题*/
- int main()
- {
- int i;
- // int c;
- int a[5];
- int c;
- printf("i %p,a[5] %p\n",&i,&a[5]); //观察栈空间是怎么分配的 这跟编译器有关系的
- printf("c %p,a[0] %p\n",&c,&a[0]);
- for(i=0;i<=5;i++)
- {
- a[i]=-i;
- printf("%d,%d",a[i],i);
- }
- return 1;
- }
l.最后说说数据结构中的栈,其实数据结构中的栈就是一个线性表,且这个线性表只有一个入口和出口叫做栈顶,还是LIFO(后进先出的)结构而已。
对于本节的函数内容其实就没什么难点了,但是对于函数这节又涉及到了顺序点的问题,我觉得可以还是忽略吧。
本节知识点:
1.函数中的顺序点:f(k,k++); 这样的问题大多跟编译器有关,不要去刻意追求。 这里给下顺序点的定义:顺序点是执行过程中修改变量值的最后时刻。在程序到达顺序点的时候,之前所做的一切操作都必须反应到后续的访问中。
2.函数参数:函数的参数是存储在这个函数的栈上面的(对于栈可以看上篇文章<内存管理的艺术>),是实参的拷贝。
3.函数的可变参数:
a.对于可变参数要包含starg.h头文件。需要va_list变量,va_start函数,va_arg函数,va_end函数。对于其他函数没什么可说的,只有va_arg函数记得一定是按顺序的接收。这里有一个可变参数使用的小例子,代码如下:
plaincopy
- #include <stdio.h>
- #include <stdarg.h>
- float average(char c,int n, ...)
- {
- va_list args;
- int i = 0;
- float sum = 0;
- va_start(args, n);
- for(i=0; i<n; i++)
- {
- sum += va_arg(args, int);
- }
- va_end(args);
- printf("%c\n",c);
- return sum / n;
- }
- int main()
- {
- char c = 'b';
- printf("%f\n", average(c,5, 1, 2, 3, 4, 5));
- printf("%f\n", average(c,4, 1, 2, 3, 4));
- return 0;
- }
b.可变参数的缺点:
(1).必须要从头到尾按照顺序逐个访问。
(2).参数列表中至少要存在一个确定的命名参数。
(3).可变参数宏无法判断实际存在的参数的数量。
(4).可变参数宏无法判断参数的实际类型。
(5).如果函数中想调用除了可变参数以外的参数,一定要放在可变参数前面。
注意:va_arg中如果指定了错误的类型,那么结果是不可预期的。
Ps:可变参数就说到这里,可变参数最经典的应用就是printf,等分析printf实现的时候,再好好写写。
4.函数与宏的比较:
注意:宏有一个函数不可取替的功能,宏的参数可以是类型名,这个是函数做不到的!代码如下:
plaincopy
- #include <stdio.h>
- #include <malloc.h>
- #define MALLOC(type, n) (type*)malloc(n * sizeof(type))
- int main()
- {
- int* p = MALLOC(int, 5);
- int i = 0;
- for(i=0; i<5; i++)
- {
- p[i] = i + 1;
- printf("%d\n", p[i]);
- }
- free(p);
- return 0;
- }
5.函数调用中的活动记录问题:包含参数入栈、调用约定等问题。见上篇文章<内存管理的艺术>。
6.递归函数:递归函数有两个组成部分,一是递归点(以不同参数调用自身),另一个是出口(不再递归的终止条件)。
对于递归函数要有一下几点注意:
a.一定要有一个清晰的出口,不然递归就无限了。
b.尽量不要进行太多层次的递归,因为递归是在不断调用函数,要不断的使用栈空间的,很容易造成栈空间溢出的,然后程序就会崩溃的。比如说:对一个已经排好序的结构进行快速排序(因为快排需要使用递归,且对排好顺序的结构排序是最坏情况,递归层数最多),就很容易造成栈空间溢出。一般不同的编译器分配的栈空间大小是不一样的,所以允许递归的层数也是不一样的!
c.利用递归函数,实现不利用参数的strlen函数。代码如下:
plaincopy
- /*这是自己实现 strlen*/
- /*
- #include <stdio.h>
- #include <stdlib.h>
- #include <assert.h>
- int my_strlen(const char *str)
- {
- int num=0;
- assert(NULL!=str);
- while(*str++)
- {
- num++;
- }
- return num;
- }
- int main(int argc, char *argv[])
- {
- char *a="hello world";
- printf("%d\n",my_strlen(a));
- return 0;
- }*/
- /*这是不用变量 实现strlen 使用递归*/
- #include <stdio.h>
- #include <stdlib.h>
- #include <assert.h>
- int my_strlen(const char *str)
- {
- assert(NULL!=str);
- return ('\0'!=*str)?(1+my_strlen(str+1)):0; //这里之所以 是加1 不是++ 我是担心顺序点的问题
- }
- int main(int argc, char *argv[])
- {
- char *a="hello world";
- printf("%d\n",my_strlen(a));
- return 0;
- }
7.使用函数时应该注意的好习惯:
a.如果函数参数是指针,且仅作为输入参数用的时候,应该加上const防止指针在函数体内被以外改变,如:
plaincopy
- void str_copy(char *strDestination,const char *strSource);
b.在函数的入口处,应尽可能使用assert宏对指针进行有效性检查,函数参数的有效性检查是十分必要的。不用assert也行,if(NULL == p)也可以。
c.函数不能返回指向栈内存的指针
d.函数不仅仅要对输入的参数,进行有效性的检查 。还要对通过其他途径进入函数体的数据进行有效性的检查 ,如全局变量,文件句柄等。
e.不要在函数中使用全局变量,尽量让函数从意义上是一个独立的模块
f.尽量避免编写带有记忆性的函数。函数的规模要小,控制在80行。函数的参数不要太多,控制在4个以内,过多就使用结构体。
g.函数名与返回值类型在语言上不可以冲突,这里有一个经典的例子getchar,getchar的返回值是int型,会隐藏这么一个问题:
plaincopy
- char c;
- c=getchar();
- if(XXX==c)
- {
- /*code*/
- }
如果XXX的值不在char的范围之内, 那c中存储的就是XXX的低8位 ,if就永远不会成立。但是getchar当然不会惹这个祸了,因为getchar获得的值是从键盘中的输入的,是满足ASCII码的范围的,ASCII码是从0~127的,是在char的范围里面的,就算是用char去接getchar的值也不会有问题,getchar还是相对安全的。可是对于fgetc和fgetchar就没这么幸运了,他们的返回值类型同样是int,如果你还用char去接收,那文件中的一些大于127的字符,就会造成越界了,然后导致你从文件中接收的数据错误。这里面就有隐藏的危险了!!!对于字符越界问题可以看看这篇文章<c语言深度解剖读书笔记(1.关键字的秘密)>8.陈正冲老师还有一个第七章是讲文件的我觉得总结不多,就写在这里了:
a.每个头文件和源文件的头部 ,都应该包含文件的说明和修改记录 。
b.需要对外公开的常量放在头文件中 ,不需要对外公开的常量放在定义文件的头部。
9.最终的胜利,进军c++(唐老师的最后一课,讲了些c++的知识,总结如下):
a.类与对象:
b.c++中类有三种访问权限:
(1).public 类外部可以*访问
(2).protected 类自身和子类中可以访问
(3).private 类自身中可以访问
小例子:
plaincopy
- #include <stdio.h>
- struct Student
- {
- protected:
- const char* name;
- int number;
- public:
- void set(const char* n, int i)
- {
- name = n;
- number = i;
- }
- void info()
- {
- printf("Name = %s, Number = %d\n", name, number);
- }
- };
- int main()
- {
- Student s;
- s.set("Delphi", 100);
- s.info();
- return 0;
- }
注意:上面这段代码要在c++的编译器中进行编译,在gcc中会报错的,因为c标准中是不允许struct中有函数的。
c.继承的使用,如图:
小例子:
plaincopy
- #include <stdio.h>
- struct Student
- {
- protected:
- const char* name;
- int number;
- public:
- void set(const char* n, int i)
- {
- name = n;
- number = i;
- }
- void info()
- {
- printf("Name = %s, Number = %d\n", name, number);
- }
- };
- class Master : public Student
- {
- protected:
- const char* domain;
- public:
- void setDomain(const char* d)
- {
- domain = d;
- }
- const char* getDomain()
- {
- return domain;
- }
- };
- int main()
- {
- Master s;
- s.set("Delphi", 100);
- s.setDomain("Software");
- s.info();
- printf("Domain = %s\n", s.getDomain());
- return 0;
- }
Ps:以上6篇文章终于更新完了,是我对陈正冲老师的<c语言深度解剖>一书和国嵌唐老师c语言课程的一些总结和理解,针对c语言,后面的一点c++仅仅是做个笔记而已,望大牛莫喷~~~
本节知识点:
plaincopy
- assert(dst && src);
也可以这样
plaincopy
- assert((NULL != dst) && (NULL != src));
上面两种方式都行!
3.给一个考指针运算的面试题吧:
plaincopy
- #include <stdio.h>
- void main()
- {
- int TestArray[5][5] = { {11,12,13,14,15},
- {16,17,18,19,20},
- {21,22,23,24,25},
- {26,27,28,29,30},
- {31,32,33,34,35}
- };
- int* p1 = (int*)(&TestArray + 1);
- int* p2 = (int*)(*(TestArray + 1) + 6);
- printf("Result: %d; %d; %d; %d; %d\n", *(*TestArray), *(*(TestArray + 1)),
- *(*(TestArray + 3) + 3), p1[-8],
- p2[4]);
- }
自己算算吧,记住一个前提就好,就是在对指针进行运算的时候一定要先弄清这个指针的类型!
4.看看下面的代码,感受下安全编程的重要性:
plaincopy
- #include<stdio.h>
- int main(int argc, char *argv[])
- {
- int flag = 0;
- char passwd[10];
- memset(passwd,0,sizeof(passwd));
- strcpy(passwd, argv[1]);
- if(0 == strcmp("LinuxGeek", passwd))
- {
- flag = 1;
- }
- if( flag )
- {
- printf("\n Password cracked \n");
- }
- else
- {
- printf("\n Incorrect passwd \n");
- }
- return 0;
- }
看看上面的代码有没有什么问题?如果把命令行输入的文字当作密码的话,会不会存在问题?答案是会,因为这里面有两个知识点:1.是数组越界 2.是strcpy安全性的问题。
plaincopy
- strncpy(passwd,argv[1],9);
最近对c语言的总结学习可以告一段落了!觉得这种边学边思考边总结的方式,还不错,还是有一定的进步的!但是对于日后的c语言学习还远远没有停止。所以写了这篇文章来督促自己对c语言的学习,告诉自己还有很多不错的书没有去读。过一段时间,再回头看看。
1.对于c语言描述的数据结构的学习。
2.林锐老师的<高质量程序设计指南>,听说他的<大学十年>也很不错,有时间应该读一读。
3.<c和指针> <c陷阱与缺陷> <c专家编程> <c++沉思录>
4.仔细研读 <c primer plus>这本书,这本书中有很多细节,很多标准(c99标准)值得学习,应该好好看看!
5.还有就是找一本介绍c 表库函数的书(像字典一样),看看c库函数都有什么,c库中有多少头文件等。
6.还有就是一些当作补充的书籍:<C语言的科学和艺术> <你必须知道的495个c语言问题> <c语言进阶:重点、难点与疑点解析> <攻破C语言笔试与机试难点V0.3>
7.最后在回头看看,带我最初接触c语言的,谭浩强的<c语言程序设计>。