这个系列是我多年前找工做时对数据结构和算法总结,其中有基础部分,也有各大公司的经典的面试题,最先发布在CSDN。现整理为一个系列给须要的朋友参考,若有错误,欢迎指正。本系列完整代码地址在 这里。html
在用C语言实现一些常见的数据结构和算法时,C语言的基础不能少,特别是指针和结构体等知识。node
linux中的C编译获得的目标文件和可执行文件都是ELF格式的,可执行文件中以segment来划分,目标文件中,咱们是以section划分。一个segment包含一个或多个section,经过readelf命令能够看到完整的section和segment信息。看一个栗子:linux
char pear[40];
static double peach;
int mango = 13;
char *str = "hello";
static long melon = 2001;
int main()
{
int i = 3, j;
pear[5] = i;
peach = 2.0 * mango;
return 0;
}
复制代码
这是个简单的C语言代码,如今分析下各个变量存储的位置。其中mango,melon属于data section,pear和peach属于common section中,并且peach和melon加了static,说明只能本文件使用。而str对应的字符串"helloworld"存储在rodata section中。main函数归属于text section,函数中的局部变量i,j在运行时在栈中分配空间。注意到前面说的全局未初始化变量peach和pear是在common section中,这是为了强弱符号而设置的。那其实最终连接成为可执行文件后,会归于BSS segment。一样的,text section和rodata section在可执行文件中都属于同一个segment。git
更多ELF内容参见《程序猿的自我修养》一书。github
想当年学习C语言最怕的就是指针了,固然《c与指针》和《c专家编程》以及《高质量C编程》里面对指针都有很好的讲解,系统回顾仍是看书吧,这里我总结了一些基础和易错的点。环境是ubuntu14.10的32位系统,编译工具GCC。面试
/***
指针易错示例1 demo1.c
***/
int main()
{
char *str = "helloworld"; //[1]
str[1] = 'M'; //[2] 会报错
char arr[] = "hello"; //[3]
arr[1] = 'M';
return 0;
}
复制代码
demo1.c中,咱们定义了一个指针和数组分别指向了一个字符串,而后修改字符串中某个字符的值。编译后运行会发现[2]处会报错,这是为何呢?用命令gcc -S demo1.c
生成汇编代码就会发现[1]处的helloworld是存储在rodata section的,是只读的,而[3]处的是存储在栈中的。因此[2]报错而[3]正常。在C中,用[1]中的方式建立字符串常量并赋值给指针,则字符串常量存储在rodata section。而若是是赋值给数组,则存储在栈中或者data section中(如[3]就是存储在栈中)。示例2给出了更多容易出错的点,能够看看。算法
/***
指针易错示例2 demo2.c
***/
char *GetMemory(int num) {
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
char *GetMemory2(char *p) {
p = (char *)malloc(sizeof(char) * 100);
}
char *GetString(){
char *string = "helloworld";
return string;
}
char *GetString2(){
char string[] = "helloworld";
return string;
}
void ParamArray(char a[])
{
printf("sizeof(a)=%d\n", sizeof(a)); // sizeof(a)=4,参数以指针方式传递
}
int main()
{
int a[] = {1, 2, 3, 4};
int *b = a + 1;
printf("delta=%d\n", b-a); // delta=4,注意int数组步长为4
printf("sizeof(a)=%d, sizeof(b)=%d\n", sizeof(a), sizeof(b)); //sizeof(a)=16, sizeof(b)=4
ParamArray(a);
//引用了不属于程序地址空间的地址,致使段错误
/*
int *p = 0;
*p = 17;
*/
char *str = NULL;
str = GetMemory(100);
strcpy(str, "hello");
free(str); //释放内存
str = NULL; //避免野指针
//错误版本,这是由于函数参数传递的是副本。
/*
char *str2 = NULL;
GetMemory2(str2);
strcpy(str2, "hello");
*/
char *str3 = GetString();
printf("%s\n", str3);
//错误版本,返回了栈指针,编译器会有警告。
/*
char *str4 = GetString2();
*/
return 0;
}
复制代码
在2.1中也提到了部分指针和数组内容,在C中指针和数组在某些状况下能够相互转换来使用,好比char *str="helloworld"
能够经过str[1]
来访问第二个字符,也能够经过*(str+1)
来访问。 此外,在函数参数中,使用数组和指针也是等同的。可是指针和数组在有些地方并不等同,须要特别注意。编程
好比我定义一个数组char a[9] = "abcdefgh";
(注意字符串后面自动补\0),那么用a[1]读取字符'b'的流程是这样的:ubuntu
那若是定义一个指针char *a = "abcdefgh";
,咱们经过a[1]来取第一个元素的值。跟数组流程不一样的是:数组
经过上面的说明能够发现,指针比数组多了一个步骤,虽然看起来结果是一致的。所以,下面这个错误就比较好理解了。在demo3.c中定义了一个数组,而后在demo4.c中经过指针来声明并引用它,显然是会报错的。若是改为extern char p[];
就正确了(固然声明你也能够写成extern char p[3],声明里面的数组大小跟实际大小不一致是没有关系的),必定要保证定义和声明匹配。
/***
demo3.c
***/
char p[] = "helloworld";
/***
demo4.c
***/
extern char *p;
int main()
{
printf("%c\n", p[1]);
return 0;
}
复制代码
typedef和#define都是常常用的,可是它们是不同的。一个typedef能够塞入多个声明器,而#define通常只能有一个定义。在连续声明中,typedef定义的类型能够保证声明的变量都是同一种类型,而#define不行。此外,typedef是一种完全的封装类型,在声明以后不能再添加其余的类型。如代码中所示。
#define int_ptr int *
int_ptr i, j; //i是int *类型,而j是int类型。
typedef char * char_ptr;
char_ptr c1, c2; //c1, c2都是char *类型。
#define peach int
unsigned peach i; //正确
typdef int banana;
unsigned banana j; //错误,typedef声明的类型不能扩展其余类型。
复制代码
另外,typedef在结构体定义中也很常见,好比下面代码中的定义。须要注意的是,[1]和[2]是很不一样的。当你如[1]中那样用typedef定义了struct foo,那么其实除了自己的foo结构标签,你还定义了foo这种结构类型,因此能够直接用foo来声明变量。而如[2]中的定义是不能用bar来声明变量的,由于它只是一个结构变量,并非结构类型。
还有一点须要说明的是,结构体是有本身名字空间的,因此结构体中的字段能够跟结构体名字相同,好比[3]中那样也是合法的,固然尽可能不要这样用。后面一节还会更详细探讨结构体,由于在Python源码中也有用到不少结构体。
typedef struct foo {int i;} foo; //[1]
struct bar {int i;} bar; //[2]
struct foo f; //正确,使用结构标签foo
foo f; //正确,使用结构类型foo
struct bar b; //正确,使用结构标签bar
bar b; // 错误,使用告终构变量bar,bar已是个结构体变量了,能够直接初始化,好比bar.i = 4;
struct foobar {int foorbar;}; //[3]合法的定义
复制代码
在学习数据结构的时候,定义链表和树结构会常常用到结构体。好比下面这个:
struct node {
int data;
struct node* next;
};
复制代码
在定义链表的时候可能就有点奇怪了,为何能够这样定义,貌似这个时候struct node尚未定义好为何就能够用next指针指向用这个结构体定义了呢?
这里要说下C语言里面的不彻底类型。C语言能够分为函数类型,对象类型以及不彻底类型。而对象类型还能够分为标量类型和非标量类型。算术类型(如int,float,char等)和指针类型属于标量类型,而定义完整的结构体,联合体,数组等都是非标量类型。而不彻底类型是指没有定义完整的类型,好比下面这样的
struct s;
union u;
char str[];
复制代码
具备不彻底类型的变量能够经过屡次声明组合成一个彻底类型。好比下面2词声明str数组是合法的:
char str[];
char str[10];
复制代码
此外,若是两个源文件定义了同一个变量,只要它们不所有是强类型的,那么也是能够编译经过的。好比下面这样是合法的,可是若是将file1.c中的int i;
改为强定义如int i = 5;
那么就会出错了。
//file1.c
int i;
//file2.c
int i = 4;
复制代码
不彻底类型的结构体十分重要,好比咱们最开始提到的struct node的定义,编译器从前日后处理,发现struct node *next
时,认为struct node是一个不彻底类型,next是一个指向不彻底类型的指针,尽管如此,指针自己是彻底类型,由于无论什么指针在32位系统都是占用4个字节。而到后面定义结束,struct node成了一个彻底类型,从而next就是一个指向彻底类型的指针了。
结构体初始化比较简单,须要注意的是结构体中包含有指针的时候,若是要进行字符串拷贝之类的操做,对指针须要额外分配内存空间。以下面定义了一个结构体student的变量stu和指向结构体的指针pstu,虽然stu定义的时候已经隐式分配告终构体内存,可是你要拷贝字符串到它指向的内存的话,须要显示分配内存。
struct student {
char *name;
int age;
} stu, *pstu;
int main()
{
stu.age = 13; //正确
// strcpy(stu.name,"hello"); //错误,name尚未分配内存空间
stu.name = (char *)malloc(6);
strcpy(stu.name, "hello"); //正确
return 0;
}
复制代码
结构体大小涉及一个对齐的问题,对齐规则为:
#pragma pack(n)
,则取最宽成员长度和n的较小值,默认pragma的n=8)的整数倍sizeof(S1) = 8, 而sizeof(S2) = 12
. 若是定义了#pragma pack(2)
,则sizeof(S1)=8;sizeof(S2)=8
typedef struct node1
{
int a;
char b;
short c;
}S1;
typedef struct node2
{
char b;
int a;
short c;
}S2;
复制代码
柔性数组是指结构体的最后面一个成员能够是一个大小未知的数组,这样能够在结构体中存放变长的字符串。如代码中所示。**注意,柔性数组必须是结构体最后一个成员,柔性数组不占用结构体大小.**固然,你也能够将数组写成char str[0]
,含义相同。
注:在学习Python源码过程当中,发现其柔性数组声明并非用一个空数组或者char str[0]
,而是用的char str[1]
,即数组大小为1。这是由于ISO C标准不容许声明大小为0的数组(gcc -pedanti
参数能够检查是否符合ISO C标准),为了可移植性,因此经常看到的是声明数组大小为1。固然,不少编译器好比GCC等把数组大小为0做为了一个非标准的扩展,因此声明空的或者大小为0的柔性数组在GCC中是能够正常编译的。
struct flexarray {
int len;
char str[];
} *pfarr;
int main()
{
char s1[] = "hello, world";
pfarr = malloc(sizeof(struct flexarray) + strlen(s1) + 1);
pfarr->len = strlen(s1);
strcpy(pfarr->str, s1);
printf("%d\n", sizeof(struct flexarray)); // 4
printf("%d\n", pfarr->len); // 12
printf("%s\n", pfarr->str); // hello, world
return 0;
}
复制代码
const int N = 3; int a[N];
这是错误的。