深入理解C语言
语言只是一种工具,任何语言之间都是相通的,一通则百通,关键是要理解语言背后的思想,理解其思想,任何语言,拿来用就行了。语言没有好坏之分,任何语言既然存在自然有它存在的价值。
在一个到处是OOP的年代,为何面向过程的C语言依然可以如此活跃?这主要得益于C语言本身的语言特性。C语言小巧灵活,而且还有一个直接与硬件打交道的指针的存在,所以它是嵌入式开发唯有的高级语言;正因为他的小巧灵活,我们可以用它来开发一系列的小工具,Unix/Linux就是由这些小工具组成的操作系统;同时用C语言可以开发高性能的应用程序。
1、数据类型。C是一门面向过程的语言,但它依旧可以实现大多数面向对象所能完成的工作。比如面向对象的三大特性:封装、继承、多态。
封装:C中有一种复杂的数据结构叫做struct。struct是C里面的结构体。
假如我们要对person进行封装,person可能包括姓名、性别、年龄、身高、体重等信息。我们就可以对它封装如下:
struct Person{
char name[];//姓名
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 = ;
p.height = ;
p.weight = ;
继承:同样利用struct,我们来创建一个学生结构,同时继承结构体Person,如下:
struct Student{
struct Person p;
char number[]; //学号
int score; //成绩
};
对Student进行创建对象,并赋值:
struct Student s;
s.p.name = "whc";
s.p.gender = 'b';
s.p.age = ;
s.p.height = ;
s.p.weight = ;
s.number = "";
s.score = ;
多态: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!!\n");
}
void printStudent(void *person){
if(NULL == person)
return ;
struct Person *p = (struct Person *)person;
printf("run in the student!!\n");
}
我们写一个函数来调用他们:
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 ;
}
他们的输出为:

其实这个也不难理解,无论是Person还是Student,他们在内存中只有一个变量,就是那个函数指针,而void*表示任何类型的指针,当我们将它强制转换成struct Person*类型时,p->print指向的自然就是传入实参的print地址。
2、 指针和内存管理
无论问哪一个C工程狮:C语言中最容易出错的地方在哪?我们基本上会得到同一个答案,那就是指针和内存溢出。那么指针是什么,指针其实就是一个地址,这个地址可以是一个变量的地址,也可以是一个函数的地址,不管是什么,反正都是内存中的一个地址。
例如有一个变量a,我们定义一个指针来保存变量a的地址:
int a = ;
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 ;
}
str0所指向的字符串就是在字符常量区,但是str0本身的这个指针变量是在栈区的,这个变量存放的是字符常量区中"Hello World!"的首地址。
str1是字符数组,所以str1中所存放的字符串是在栈区,这里利用的不过是字符数组初始化的一种形式,其实它可以写成如下形式:
char str1[] = {'H','e','l','l','o',' ','W','o','r','l','d','!','\0'};
(4)栈区:局部变量,形参,函数返回地址等,由系统来管理,在内存里面是由高地址往低地址生长,所以栈空间大小是有限的,当在栈中定义一个很大的数组或者使用很深的递归调用时,就有可能栈溢出。
(5)堆区:由malloc、calloc、realloc函数分配的空间,由我们自己来管理,每次用完之后,必须用free释放内存,否则,就会产生内存泄漏,每次释放内存后,虽然不再占用着这块内存中,但是对应的指针依然指向这块区域,这个指针就是野指针,所以释放内存后,建议给指针赋NULL。如下:
int main(){
int *p = (int*)malloc(*sizeof(int));
/*
执行语句
*/
free(p);//这时p依然指向那块内存,成了野指针
p = NULL; //对p赋值NULL
return ;
}
3、C语言的I/O输入输出
C语言本身并不带有输入输出的特性,所以它的所有I/O操作都是通过系统调用来实现。幸运的是C标准库,已经给我们封装好了一系列的I/O操作的函数。
putchar ():把变量中的一个字符常量输出到显示器屏幕上;
getchar ();从键盘上输入一个字符常量,此常量就是该函数的值;
printf ();把键盘中的各类数据,加以格式控制输出到显示器屏幕上;
scanf ();从键盘上输入各类数据,并存放到程序变量中;
puts ():把数组变量中的一个字符串常量输出到显示器屏幕上
gets ():从键盘上输入一个字符串常量并放到程序的数组中
一些为对文件的操作,由于一切皆可看作是文件,标准输入,输出也可以当作文件来操作,文件描述符:标准输入(0)、标准输出(1)、标准错误(2)
fputs();输出到文件
fgets();从文件输入
fscanf();格式化文件输入
fprintf();格式化文件输出
另外两个很重要的函数,当然还有他们的派生函数也是类似的
sscanf(); 从一个字符串中提取各类数据。
sprintf(); 把格式化的数据写入某个字符串
这里不对每个函数进行详解,主要对格式化函数进行分析:
(1)当我们要把一个字符串转换成一个整数或者把一个整数转换成一个字符串时,我们一般会想到atoi()或者itoa()(非标准函数),但是我们可以通过流来实现:
int main(){
int num = ;
char str[] = {};
sprintf(str,"%d",num); //把int转换成char[]
num = ;
sscanf(str,"%d",&num);//把字符串转换成int
printf("num:%d str:%s\n",num,str);
return ;
}
输出结果如下:

把字符串转与其它类型之间的转换:比如float,16进制,unsigned等都可以用流实现。
(2)格式化函数中的正则表达式
所有的格式化函数都可以定制自己的扫描集 %[abc]、%[a-z]、%[^abc]、%[^a-z],其中[]内是匹配的字符,^表示求反集。
当我们要从标准输入输入一个可能带空格的字符串时,直接用scanf("%s",str);当读到空格时就返回,此时就可以使用正则表达式:
char str[] = {};
scanf("%[^\n]",str);//直到遇到回车才写入
从标准输入中只要读小写字母a-z,遇到其它字符则返回:
char str[] = {};
scanf("%[a-z]",str);
其他格式化函数的用法相同,不一一举例。
4、总结
从大一开始学习C语言也有四五年了,个人认为:C语言中最大的成功在于它的指针,但是也是最容易出错的,想要理解C,必须要掌握指针。虽然说,语言只是一门工具,但是这是基础。或许,你可以说,现在是JAVA的天下了,满大街都是招聘JAVA工程师;或者你可以说C太底层,现在都是OOP的时代了,谁还会用面向过程的......你们不要忘了操作系统是用什么写的?是C;C实现的nginx的并发量是C++实现的apache的几十倍。无论是什么编程语言,好好学,深入学就行,不要因为它今天流行就抛弃昨天所学的。
版权所有,欢迎转载,转载请注明出处。
深入理解C语言的更多相关文章
- 【转载】理解C语言中的关键字extern
原文:理解C语言中的关键字extern 最近写了一段C程序,编译时出现变量重复定义的错误,自己查看没发现错误.使用Google发现,自己对extern理解不透彻,我搜到了这篇文章,写得不错.我拙劣的翻 ...
- 深入理解c语言_从编译器的角度考虑问题_纪念Dennis Ritchie先生
开源中国: Dennis Ritchie教授过世了,他发明了C语言,一个影响深远并彻底改变世界的计算机语言.一门经历40多年的到今天还长盛不训的语言,今天很多语言都受到C的影 响,C++,Java,C ...
- 深入理解C语言的函数调用过程 【转】
转自:http://blog.chinaunix.net/uid-25909619-id-4240084.html 原文地址:深入理解C语言的函数调用过程 作者:wjlkoorey258 本文 ...
- "深入理解C语言" 指针
本文对coolshell中的"深入理解C语言"这篇文章中提到的指针问题, 进行简要的分析. #include <stdio.h> int main(void){ ]; ...
- 理解C语言中指针的声明以及复杂声明的语法
昨天刚把<C程序设计语言>中"指针与数组"章节读完,最终把心中的疑惑彻底解开了.如今记录下我对指针声明的理解.顺便说下怎样在C语言中创建复杂声明以及读懂复杂声明. 本文 ...
- 这样子来理解C语言中指针的指针
友情提示:阅读本文前,请先参考我的之前的文章<从四个属性的角度来理解C语言的指针也许会更好理解>,若已阅读,请继续往下看. 我从4个属性的角度来总结了C语言中的指针概念.对于C语言的一个指 ...
- 深入理解C语言 - 指针使用的常见错误
在C语言中,指针的重要性不言而喻,但在很多时候指针又被认为是一把双刃剑.一方面,指针是构建数据结构和操作内存的精确而高效的工具.另一方面,它们又很容易误用,从而产生不可预知的软件bug.下面总结一下指 ...
- 通过实战理解C语言精要——函数篇
前言 本篇博客是对C语言函数部分的重点内容和细枝末节通过实战得到的经验的总结精炼,不涵盖C语言函数的全部内容,所有提炼内容均来自提炼与实战,阅读需要对函数部分有一定基础,可用于对C语言函数的理解提 ...
- 深入理解 C 语言的函数调用过程
来源: wjlkoorey 链接:http://blog.chinaunix.net/uid-23069658-id-3981406.html 本文主要从进程栈空间的层面复习一下C语言中函数调用的具体 ...
随机推荐
- Guava学习笔记:Immutable(不可变)集合
不可变集合,顾名思义就是说集合是不可被修改的.集合的数据项是在创建的时候提供,并且在整个生命周期中都不可改变. 为什么要用immutable对象?immutable对象有以下的优点: 1.对不可靠的客 ...
- TestNG官方文档中文版(2)-annotation
TestNG的官方文档的中文翻译版第二章,原文请见 http://testng.org/doc/documentation-main.html 2 - Annotation 这里是TestNG中用到的 ...
- mysql 5.6 read-committed隔离级别下并发插入唯一索引导致死锁一例
今天,某个环境又发生了死锁,如下: *** (1) TRANSACTION:TRANSACTION 735307073, ACTIVE 0 sec insertingmysql tables in u ...
- ShareDrop – 苹果 AirDrop 服务的 HTML5 实现
ShareDrop 是苹果 AirDrop 服务的 HTML5 版本,你可以直接在设备之间传输文件,而无需先上传到任何服务器.它使用 WebRTC 来实现安全的点对点文件传输.目前 ShareDrop ...
- 原生JS:Math对象详解
Math对象 本文参考MDN做的详细整理,方便大家参考MDN Math 也是一个内置对象, 为数学常量和数学函数提供了属性和方法,而不是一个函数对象. 与其它全局对象不同的是, Math 不是一个构造 ...
- Flex Viewer
一.Flex Viewer简介 Flex Viewer是ESRI公司推出的可以高效开发基于WEB的地理信息应用系统的一种完全免费的应用程序框架.业务人员使用该框架可以无需任何额外的编程就能够通过简单配 ...
- Python 操作 MySQL 之 pysql 与 ORM(转载)
本文针对 Python 操作 MySQL 主要使用的两种方式讲解: 原生模块 pymsql ORM框架 SQLAchemy 本章内容: pymsql 执行 sql 增\删\改\查 语句 pymsql ...
- [转]Android逆向之动态调试总结
一.在SO中关键函数上下断点 刚学逆向调试时.大多都满足于在SO中某关键函数上下断点.然后通过操作应用程序,去触发这个断点,然后进行调试 详细的步骤可以参见非虫大大的<Android软件安全与逆 ...
- Android 创建自己的Camera App
在sdk中找到/sdk/docs/guide/topics/media/camera.html#custom-camera,里面有详细的api参考 在清单文件中添加相应的权限: <uses-pe ...
- Android 网络HTML查看器
本文实现一个基于Android的网络HTML查看器 新建项目,项目布局文件如下: <LinearLayout xmlns:android="http://schemas.android ...