语言只是一种工具,任何语言之间都是相通的,一通则百通,关键是要理解语言背后的思想,理解其思想,任何语言,拿来用就行了。语言没有好坏之分,任何语言既然存在自然有它存在的价值。
在一个到处是OOP的年代,为何面向过程的C语言依然可以如此活跃?这主要得益于C语言本身的语言特性。C语言小巧灵活,而且还有一个直接与硬件打交道的指针的存在,所以它是嵌入式开发唯有的高级语言;正因为他的小巧灵活,我们可以用它来开发一系列的小工具,Unix/Linux就是由这些小工具组成的操作系统;同时用C语言可以开发高性能的应用程序。
1、数据类型。C是一门面向过程的语言,但它依旧可以实现大多数面向对象所能完成的工作。比如面向对象的三大特性:封装、继承、多态。
封装:C中有一种复杂的数据结构叫做struct。struct是C里面的结构体。
假如我们要对person进行封装,person可能包括姓名、性别、年龄、身高、体重等信息。我们就可以对它封装如下:
struct Person{ char name[20];//姓名 char gender; //性别 int age; //年龄 int height; //身高 int weight; //体重 };
当我们要像OOP那样新建一个对象时,我们就可以:
struct Person p;
我们就可以直接对p进行赋值:
p.name = "whc"; p.gender = 'b'; //'b' = boy; 'g' = girl p.age = 25; p.height = 175; p.weight = 65;
继承:同样利用struct,我们来创建一个学生结构,同时继承结构体Person,如下:
struct Student{ struct Person p; char number[20]; //学号 int score; //成绩 };
对Student进行创建对象,并赋值:
struct Student s; s.p.name = "whc"; s.p.gender = 'b'; s.p.age = 25; s.p.height = 175; s.p.weight = 65; s.number = "20150618"; s.score = 90;
多态:C中对于多态的实现可以借助函数指针来实现。为了简单起见,我们假设Person这个结构体中,只有一个函数指针。
struct Person{ void (*print)(void *p); }; struct Student{ struct Person p; };
而Person和Student这两个结构体的print函数实现如下:
void printPerson(void *person){ if(NULL == person) return ; struct Person *p = (struct Person *)person; printf("run in the person!! "); } void printStudent(void *person){ if(NULL == person) return ; struct Person *p = (struct Person *)person; printf("run in the student!! "); }
我们写一个函数来调用他们:
void print(void *person){ if(NULL == person) return ; struct Person *p = (struct Person *)person; p->print(person); } int main(){ struct Person person; struct Student student; person.print = printPerson; student.p.print = printStudent; print(&person); //实参为Person的对象 print(&student); //实参为Student的对象 return 0; }
他们的输出为:
其实这个也不难理解,无论是Person还是Student,他们在内存中只有一个变量,就是那个函数指针,而void*表示任何类型的指针,当我们将它强制转换成struct Person*类型时,p->print指向的自然就是传入实参的print地址。
2、 指针和内存管理
无论问哪一个C工程狮:C语言中最容易出错的地方在哪?我们基本上会得到同一个答案,那就是指针和内存溢出。那么指针是什么,指针其实就是一个地址,这个地址可以是一个变量的地址,也可以是一个函数的地址,不管是什么,反正都是内存中的一个地址。
例如有一个变量a,我们定义一个指针来保存变量a的地址:
int a = 0; int *p = &a;
如果是一个函数呢?我们定义一个函数,然后用一个函数指针来保存这个函数地址:
int min(int a,int b){ return a<b?a:b; } int (*f)(int,int); f = min;
可能我们有时候会想,难道我们只能先定义一个变量或者函数,然后把它的地址给指针么?不能直接使用指针,或者直接给指针赋一个常量么?首先,我们不知道内存中哪些是可用的地址,哪些是不可用的,每当我们定义一个指针时,这个指针指向的是一个未定义的内存,这个就是传说中的野指针。如果我们给这个指针所指向的内存赋值,就有可能覆盖了一些很重要的数据,所以每当我们定义一个指针时,最好给它赋一个初始地址或者NULL;如果我们给一个指针赋常量,同样的道理。
指针的类型要与变量的类型一致(如果我们不是故意要他们不一致),所谓类型,只是变量的一直表现形式,其实在内存中,他们不过是0101的二进制,当我们用32bits的原码表示时,它就是unsigned;当我们用32bits补码表示时,就是signed;当用浮点表示时就是float;当用更复杂的自定义表示时就是struct;用union可以很好的理解这些。
现在我们来讲一下内存,这里我们只讨论用户内存区域:
一般分为5个区域:
(1)程序代码区:存放代码指令的地方
(2)全局(静态)变量区:包括初始化、未初始化的全局变量和静态变量
(3)字符常量区:存放一些字符串常量,在C语言里面,这个很容易与栈中定义的字符数组搞混,当我们定义如下:
int main(){ char *str0 = "Hello World!"; //字符常量区 char str1[] = "Hello World!"; //栈区 return 0; }
str0所指向的字符串就是在字符常量区,但是str0本身的这个指针变量是在栈区的,这个变量存放的是字符常量区中"Hello World!"的首地址。
str1是字符数组,所以str1中所存放的字符串是在栈区,这里利用的不过是字符数组初始化的一种形式,其实它可以写成如下形式:
char str1[] = {'H','e','l','l','o',' ','W','o','r','l','d','!','