本文的动机是平时看的资料中涉及到很多c/c++的实现,比如node.js源码,因此这里会对c的基本使用有个介绍,会假定读者已经有基本的js和其他计算机基础,参考 C Primer Plus(第6版) 。
概述
编程语言(programming language)和其他语言一样都是一种用于交流的工具,编程语言用于人类和机器(主要是计算机)之间的沟通,人们会将自己的想法按照特定语言的规范进行表达,通过解释或编译,通过最终会作为机器语言的指令被机器执行。
每种语言为了解决特定问题,都会实现对应编程范式,编程语言实现的编程范式反过来限制了一门语言的代码组织形式和使用场景。c语言实现了命令式编程范式,并可以利用结构化编程提高代码可读性和开发质量。
这里我们介绍的c语言是一种偏底层的高级语言,可以跨平台、接近硬件、性能高等优点,unix operating system是c语言的第一个主要应用。
结构化编程
结构化编程(Structured programming)也被称为一种编程范式,可以看成一种代码组织形式,对应的是非结构化编程(Non-structured programming),后者利用goto等语句跳转到指定标签,从而产生难以阅读的意大利面条式代码(Spaghetti code)。
结构化编程利用控制结构(Control structures)、子程序(Subroutines)和块(Blocks)使得代码更清晰。
Control structures
按照结构化编程的代码是由各种控制结构组成的,具体结构包括
- 顺序(Sequence):有序语句或按序执行的子程序
- 选择(Selection):一个或者一系列语句的执行取决于程序的状态,比如if语句
- 迭代(Iteration):当满足一定条件就会对一个集合的每一个元素执行某个操作,比如while语句
- 递归(Recursion):一个语句重复调用自身,直到遇到终止条件
Subroutines
子程序即可调用单元,比如procedures、函数、方法等
Block
块可以将一组语句被当做一个语句对待,
程序生成过程
这里介绍常规的步骤。
c语言的源代码以.c为后缀(.h为后缀的头文件内容会在编译前添加到源代码中一起编译),同一个应用的源码可以写在不同文件中进行模块化,首先通过编译器可以将源代码文件编译成目标代码文件,后者是源代码对应的机器语言组成的文件。
目标代码缺少两种代码
- 启动代码(startUp code),启动代码是程序和操作系统之间的接口,不同操作系统的启动代码不一样
- 库函数,比如我们常用的
printf()
函数的具体实现
因此接下来需要链接器将目标文件和其他两部分合并成最终的可执行文件
数据类型
计算机程序主要由算法和数据结构组成,一个特定类型的数据结构被称为一种数据类型。c语言中的数字类型包含以下(这里的分类按照菜鸟教程)
基本类型
基本数据类型分为两大类,整数和浮点数,数据通常分为有符号和无符号,另外还有几种特殊的整数类型
整数类型
常用的包括int,short,long,long long,后两个分别表示short int、long int和long long int,具体位数根据具体系统而定,但有以下规定
- int 不少于16位
- short 至少16位,不大于int长度
- long 至少32位,不小于int长度
- long long 不小于long,至少64位
浮点类型
常用的包括float,double,long double,具体位数
- float 至少六位有效数
- double 至少10位有效数
- long double 比double位数多
char
一个char表示一个字符,通常占8位,但为了表示更多字符,可能需要更多位。
_Bool
布尔类型通常占1位,用0表示false,1表示true
枚举类型
枚举类型用来通过声明符号名称来表示整数类型,通常从1开始,使用enum关键字,比如
enum DAY
{
MON, TUE, WED, THU, FRI, SAT, SUN
};
其中MON表示1,其他递增。
void
比如用于表示没有返回值或没有参数
派生类型
这部分类型会在后面章节详细展开,派生类型就是利用基本类型组合而成的类型,像基本类型一样使用
- 指针类型
- 数组类型,这里还会讨论字符串
- 结构类型
- 联合类型
- 函数类型
基本使用
这里讲一下基本c的基本使用,以一个hello world
程序为例
#include <stdio.h>
int main()
{
/* 我的第一个 C 程序 */
int a=3;
printf("%d",a);
return 0;
}
其中
- 以井号(#)开头的语句被称为预处理指令,会在编译之前执行
int main()
是主函数,表示该程序的入口printf()
是库函数提供的输出函数
类型声明
和js的动态类型不同,c语言中使用到的每个变量都需要首先进行类型声明,比如int a
表示变量a是int
类型。
声明时使用const可以声明常量,不仅对于基本类型有效,比如
const int a[8] ={1,3};
a[1]=8;//error: assignment of read-only location ‘a[1]’
预处理指令
在编译器编译之前需要先使用预处理器根据预处理指令进行对应操作,每个指令包含当前逻辑行,每个逻辑行可能包含多个物理行,比如转义符后面的换行符会被删除,
printf("abc\
d");
//abcd
预处理指令最常用的包括以下两个
#define
可以用来定义常量,其他代码中使用到该常量的地方会被具体值替换,比如#define A 3
表示使用A的地方用3来替换。其中的常量也被称为宏。
除了定义常量还可以用来定义其他的,比如宏函数。
#include <stdio.h>
#define PR(x) printf("w" x);
int main()
{
PR("a");
return 0;
}
#include
用来将指定的文件包含在当前文件中,替换该指令的位置,该指令包含两种语法
- 文件名在尖括号中
- 文件名在双引号中
其中前者表示在标准系统目录中查找,后者会在指定目录中查找,找不到的话再查找标准系统目录。
该指令一般用来引入头文件,头文件一般为.h
后缀,常包含以下内容
- 常量
- 宏函数
- 函数声明
- 结构模板
- 类型定义
输入和输出
程序数据的输入和输出被称为i/o.包括命令行输入输出或文件输入输出,这里讨论两个用于输出输出的函数
printf
语法为printf(格式字符串,待打印项1,待打印项2,...)
,其中格式字符串中使用%表示类型的字母
来指定待打印项的数据类型,常见的类型表示包括
- %d 有符号10进制
- %p 指针
- %s 字符串
比如
#include <stdio.h>
int main () {
int a=9;
printf("hello %d ee\n",a);//hello 9 ee
return 0;
}
scanf
输入不同格式的数据,语法和printf类似,但要注意两点
- 当读取基本变量的值,要在变量名前加一个
&
- 当把字符串读入字符数组中,不用加
输入多个字段时,使用换行符、空格或tab分隔,除了%c
。
运算符
运算符表示算术运算。
其中基本运算符分为赋值运算符和四则运算,其他运算符包括
- sizeof 可以用来计算特定类型或者特定值得字节数
- % 取模
- 递增和递减
表达式
表达式(expression)由运算符和运算对象组成,每个表达式都有一个值,注意声明不是表达式。
语句
语句(statement)是c程序得基本构建块,一条语句相当于一条完整的计算机指令,大部分语句都以分号结尾,表达式也是语句的一种。
复合语句是用花括号括起来的一条或多条语句,也被称为块(block),这里其实和js差不多,因此介绍会简略。
循环语句
- while
- for
- do...while
控制语句
- if/if else
- 逻辑运算符,或与非(||,&&,!)
- 条件运算符,?:
- 循环辅助,break,continue
- 多重选择,switch break
- goto语句 跳到指定标签,一般不要用
goto part2;
parts:printf("test");
指针
指针可以看作是一种派生类型,指针变量用来保存特定类型变量的地址。在c语言中并没有引用类型,通过指针可以操作相关类型的引用,比如数组和结构的引用。
这里会介绍指针的基本用法,后续会结合其他派生类型讲述指针的其他用法。
声明指针
声明一个指针时就像声明一般变量一样,但是类型和变量中加一个星号,表示保存该类型数据的指针,比如
int * p;//指向int类型变量的指针
地址运算符
还可以通过地址运算符&
获取对象变量的地址
#include <stdio.h>
int main () {
int a=2;
int *p=&a;
printf("hello %p ee\n",p);
return 0;
}
解引用
地址前用星号获取当前地址保存的值
int b=*p;
千万不要解引用未初始化的指针,因为未初始化的指针是随机值,可能会擦写其他数据。
int *pt;
*pt=5;//不要这样
数组
数组由数据类型相同的一系列元素组成,使用数组之前需要提前告诉编译器含有多少元素和这些元素的类型,数组的值用花括号内的逗号分隔的元素表示,比如
int a[8] ={1,3};
printf("数组字节数为:%d",sizeof a);//数组字节数为:32
当中括号中数字忽略,则以初始值实际个数为准,如果实际个数小于对应数字,则余位补0
多维数组
声明时使用多个中括号表示多维数组,比如
int a[][2]={{1,3}}
指针与数组
数组名就是数组首元素的地址,可以赋值给指针变量,即如果flizny是一个数组,则
flizny==&flizny[0];
我们可以对指针有一些基本操作
- 赋值 把地址赋给指针
- 解引用 通过星号获取指针对应地址保存的值
- 取址 和其他类型的变量一样,指针也有地址,也可以通过
&
获取指针被保存的地址,这里要和指针变量的值区分开,两者都是地址 - 指针与整数相加 整数会先和指针对应的类型大小相乘,然后再在指针保存的地址基础上相加,可用来对数组操作
- 指针与整数相减
- 递增指针 递增指向数组元素的指针可以让该指针移动至数组的下一个元素
- 递减指针
- 指针求差
- 比较 前提被比较的两个指针指向同一种类型
字符串
字符串是以空字符(\0)结尾的char类型数组,可以通过字符串字面量 即用双引号括起来的内容,编译器会在末尾自动加空字符
char greet[50]="hello"
相较于使用字符数组
char greet[50]={'h','e','l','l','o','\0'}
结构
结构使用struct
关键字,类似于ts中的tuple,即可以在不同位置保存不同类型数据的数组,但又想对象一样使用.
来访问其成员。
结构声明
声明时需要结构名和各个成员的类型,结果是创建一个新的数据结构,比如
struct books{
char name[7];
int price;
}
初始化
可以把新的结构名作为普通数据类型使用,结构名前面要加struct关键字
struct books book={"html",23};
也可以在结构声明时初始化。
struct books{
char name[7];
int price;
} book={"html",23};
访问
访问时可以用.
访问,比如book.price
结构与指针
结构和数组不同,结构名并不是指针,因此想要获取地址需要使用取址符,访问成员时可以使用->
,比如
struct books *p=&book;
printf("价格为%d",p->price);//价格为23
联合类型
使用关键字union,能在同一个内存空间存储不同类型的值,在同一时刻只能存储其中一种。
union books{
char name;
int price;
};
union books book;
book.price=33;
book.name='a';
指针的使用类似结构
函数
函数是完成特定任务的独立程序代码。
函数中的返回值和参数要标明类型,比如
int add(int a,int b){
return a+b;
}
c语言中的函数声明不会提升,因此如果函数定义在调用之前,正常使用,如果函数定义在使用之后,需要在使用之前使用函数声明或叫函数原型告知编译器对应函数的签名,函数声明就是没有函数体的函数定义,也可以省略形参,比如上面的函数的声明为
int add(int,int);
传参方式
调用参数时可以有两种方式,一种是按值传递,一种是按地址传递,其中后者可能会改原数据,但避免了另外拷贝一份。
内存管理
c语言可以对内存更好的管理
存储类别
一个变量,我们关注它的三个方面,一个是存储的值、变量名和地址,这里要注意下,变量名只是一种开发者使用的标识符,最终和变量对应的还是地址。 我们可以用作用域(scope)和链接(linkage)描述标识符,用来表明程序的哪些部分可以用它。
被存储的值都会占用一定的内存,这块内存被称为对象,注意这个对象和面向对象的不是一个对象。我们可以用存储期(storage duration)来描述对象,表示对象在内存中保留了多少时间。
相关概念
作用域
作用域描述程序中可以访问标识符的区域,包括块作用域、函数作用域、函数原型作用域和文件作用域。
其中块是一对花括号括起来的代码区域,比如函数体。
函数作用域仅用于goto语句的标签,这意味着即使一个标签首次出现在函数的内层块中,其作用域也会延伸到整个函数。
函数原型作用域是从形参定义处到函数声明结束,比如在后面的形参中可以用前面形参中的标识符。
当变量的定义在函数外面则有文件作用域,文件作用域变量又称为全局变量。
链接
链接属性
- 没链接(none) 表示该标识符的多个声明被当做不同个体
- 内部链接(internal) 前三种作用域的标识符具有内部链接,这些标识符属于这些作用域私有
- 外部链接(external) 文件作用域的标识符可以是内部链接也可以是外部链接
存储期
c语言中的存储期有四种
- 静态存储期 程序执行期间一直存在,比如文件作用域的变量
- 线程存储期 用于并发编程
- 自动存储期 会自动分配和回收,比如块作用域,其中块作用域中的变量添加static关键字就会变成静态存储期
- 动态分配存储期 手动分配内存
具体类别
根据以上概念,可以把存储类别分为
自动变量
默认在块或函数头的任何变量都属于自动变量,还可以显式使用auto关键字
寄存器变量
变量通常在内存中,存在寄存器的变量访问更快,可以使用register关键字主动请求,如果没放到寄存器,就会变成普通的自动变量。
块作用域的静态变量
静态变量指的是在内存中不动的变量,具有文件作用域的变量是且仅是静态静态存储期。块作用域中的变量可以通过static显式将变量声明为块作用域、无链接的静态变量,比如可以用于函数的多次调用时进行基数。
外部链接的静态变量
这种变量具有文件作用域、外部链接和静态存储期,该类别又称外部存储类别,对应变量称为外部变量。当多个文件共用一个外部变量时,除了一个定义声明,其他地方要使用extern声明。
如果想在同一个文件内想在定义之前使用外部变量,也需要使用extern声明。
即如果在当前作用域访问不到外部变量,就使用extern声明。extern对函数无效,函数除非使用static,否则默认extern。
内部链接的静态变量
和前一种的区别是内部链接,使用static声明,只能用于一个文件内。
存储类别的选择
大部分场景使用自动存储类别,外部变量存在污染全局作用域的问题,如有必要使用const。
内存分配
前面讨论的每种存储类别都有自己的内存管理规则,这里重点讨论存储期中的动态分配存储期,即如何手动分配和释放内存。
之前我们对相关类型的数据声明后,系统会自动分配相关内存,我们可以使用malloc
函数手动分配内存,该函数声明为以下,会返回一个指向所分配内存(这里用void表示)的指针
void *malloc(size_t size)
用malloc分配的内存,使用free()回收,函数声明为
void free(void *ptr)
还有calloc 或 realloc等分配方式。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
char *str;
/* 最初的内存分配 */
str = (char *) malloc(15);
strcpy(str, "runoob");
printf("String = %s, Address = %p\n", str, str);
/* 重新分配内存 */
str = (char *) realloc(str, 25);
strcat(str, ".com");
printf("String = %s, Address = %p\n", str, str);
/* 释放已分配的内存 */
free(str);
return(0);
}
总结
c语言是一个基础语法很简单的语言,除了指针等一小部分概念,其他的都在ts中接触过,因此入门比较简单。
c语言中没有面向对象,也没有引用类型,取而代之的是,利用指针将内存,利用结构化编程将数据,直接展示在我们面前,没有面向对象的那一层抽象层,灵活度也获得提升,同时也带来了各种边界问题。
后面可能会对包括c++在内的其他语言做一下入门。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!