小白快速学C语言
C 语言是一种通用的、面向过程式的计算机程序设计语言。由于C语言是一个较为底层的编程语言,通过学习C语言,能够快速的从操作系统层面到编程语言层面有个宏观的理解,是各大高校与计算机相关方面研究生的必备语言之一。
本教程服务于编程小白、大一新生、研究生考试等基础人群,旨在通过最简短的例子与说明让其快速读懂、书写简单的C语言,是一个Boost学习方案。如若有充裕的时间,作者诚恳的建议从零学起,扎实基础。
本教程借鉴了菜鸟教程的相关内容,菜鸟教程致力于编程技术普及化,是一个极好的互联网技术学习网站。
目录
- 基础语法
- 数据类型
- 变量与常量
- 运算符、判断、循环
- 函数、作用域、头文件
- 结构体、共用体
- 指针、数组、字符串、输入输出流、内存管理
基础语法
C语言的基础知识
-
C语言程序由函数构成,每个函数可以实现一个或多个功能。
-
一个正规程序可以有多个函数,但是有且只有一个主函数(main)。
-
函数只有在被调用的时候才执行,主函数由系统调用执行。
-
函数的格式必须按照规范书写。
-
C 语言程序文件的后缀为 .c 或 .h。
我的第一个C语言程序
#include <stdio.h>
int main(int argc, char *argv[]){
printf("Hello World!!\n");
return 0;
}
C语言的本质是令牌语言:关键字、标识符、常量、字符串值、符号等令牌组成。
一条语句的拆解:printf
(
"Hello World!!\n"
)
;
标识符(变量)命名规则
一个标识符以字母A-Z
或 a-z
或下划线 _
开始,后跟零个或多个字母、下划线和数字(0-9),请注意变量名不能为保留字(关键字、基础类型等)。
int a; int _a; int A; int a1; // 正确的变量命名
int 1; int 1a; int void; int if; // 错误的变量命名
常用的标识符命名规则如下:
- 变量:建议全小写或驼峰命名(第一个单词首字母小写其余单词首字母大写)
- 函数:帕斯卡(Pascal)命名法(所有首字母大写)
- 常量:建议定义为宏(宏字母全大写)
- 结构体等:建议全大写或帕斯卡(Pascal)命名法
标识符命名规则不唯一,请遵循开发文档,或编写自己的程序时统一标准。
C语言的注释
C语言有两种注释的方式:单行注释与多行注释。
// 单行注释
/*
多行注释
*/
C语言被注释的文字将被解释为空格存在, 而C语言会自动忽略所有空格,从而达到注释的目的。
数据类型
数据类型是语言内精髓的存在,是经过多年的版本迭代最优的考究,因此每一门语言的数据类型都值得去仔细学习,此文章只用于知识普及与快速上手编程,在此不作详细说明。
基本数据类型
int i = 0; // 整型数据
char ch = 'a'; // 字符型数据/整型
float f = 3.14f; // 浮点型数据
double d = 3.14; // 双精度浮点型数据
浮点型数据不以f
或F
结尾默认以双精度浮点型形式存在。
枚举类型
枚举类型用于定义一组具有离散值的常量(同数据类型)。
枚举类型常与switch语句结合使用,在后续会提及。
// 定义枚举类型
enum MyBool
{
FALSE = 0, // 初始化第一个枚举项
TURE // 后续枚举项会自动初始化为上一个枚举量的自加(+1)值
};
// 定义枚举变量
enum MyBool _bool;
void类型
顾名思义,void类型即表示什么类型都不是,通常用于函数参数、指针指向类型等。无类型不代表无用,在很多场景下用处非常大。
void func(void)
{
void *ptr = NULL; // NULL在C语言中等同于0
return;
}
其他派生类型
C语言中还有许多派生类型:数组类型、指针类型、结构体类型。在此只说明如何定义,在后续会重点介绍用法与特性。
int a[10] = {0}; // 数组类型
int *ptr = NULL; // 指针类型
typedef struct MyStruct // 结构体类型
{
int i;
} MyStruct;
无符号类型
标准整型都支持定义对应的无符号类型
unsigned int ui = 1; // 无符号指的是无符号,即不能表示负数,正数上限增大(0~255)
signed int si = 1; // 有符号与之对应(-128~127)
unsigned char uch = 'a';
思考一个问题:C语言有符号int整型变量中 0
是怎么存储的,有 -0
吗?
类型转换
// 隐式类型转换:自动转换(小数据到大数据)
int i = 10; // 4字节大小
float f = 3.14; // 4字节大小
double d = i + f; // d是8字节大小,隐式转换i
// 显式类型转换:手动强制转换(大数据到小数据),容易产生数据丢失或截断
d = 3.14159; // d是8字节大小
i = (int)d; // i是4字节大小,显式转换d
类型转换中涵盖了很多知识,包括底层内存是怎么操作的?什么是强制类型转换?一定会转换成功吗?转换失败了怎么办?不同数据类型的可以转换吗?等等问题。当然在此作者不过多阐述,想要了解的同学经过系统性的学习一定要清楚上述问题的答案。
变量与常量
对于C语言初学者来说,变量真是一个晦涩难懂的概念。变量其实只不过是程序可操作的存储区的名称,C语言中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。
举个例子。现在需要定义一个int类型(4字节)的变量 i
,则需要在内存中开辟四字节大小的空间,如果初始化 i
为 1
,则需要把 1
存入这4字节的内存空间中,当然是以二进制形式存入。这两就完成了一个int类型变量的定义及初始化,而 i
即为这个变量的变量名。
变量的底层存放实现原理比较复杂,需要一定的操作系统或计算机组成原理知识去加以理解,在后续关于指针的教程中会再次提及,不用担心现在看不懂。
变量的类型
局部变量:在局部作用域定义的普通变量。
void func()
{
int local_1; // 存在函数局部作用域内
{
int local_2; // 存在规定的局部作用域内(也存在函数局部作用域内)
}
}
注意:作用域将会在后续中再次提及说明,在此你可以简单的理解为在{}
运算符内即为在某个作用域内。
全局变量:定义在所有函数体外,与上述相反,不存在于任何函数或其他规定的作用域内。
int global; // 不存在于任何作用域内
静态变量:使用static关键字修饰的变量(包含全局变量与静态变量)
static int static_global; // 静态全局变量
void func()
{
static int static_local; // 静态局部变量
}
变量的定义与初始化
变量的初始化是在定义变量的同时为其赋予一个初始值。变量的初始化可以在定义时进行,也可以在后续的代码中进行。
type variable_list; => 数据类型 一个或多个变量的名称组成
int a; // 定义未初始化
a = 0; // 变量初始化
int i, j, k, m = 0; // 直接初始化
int *ptr = NULL; // 直接初始化
注意:
- 全局变量与静态变量若未显式初始化会自动初始化为类型默认值
- 局部变量不会自动初始化,未初始化时指向栈上一个随机值
- 使用未初始化的变量极其危险,局部变量一定记得初始化
静态变量的初始化与使用的经典代码案例:
// part 1
static int static_i = 0;
while (x--)
{
static int static_i = 0;
static_i++;
}
printf("%d\n", static_i);
// part 2
int res;
int x = 10;
while (x--)
{
static int static_i = 0;
res = ++static_i;
}
printf("%d\n", res);
初识左值与右值
左值与右值的定义是C语言乃至C++语言都较为灵魂的存在,千言万语不足以概括其特点与应用场景,在此旨在用最简短的语言说明。
左值:可以出现在 =
的左边或右边,有内存、有名字、非临时。
// 变量a、变量b都是左值
int a = 0;
int b = a;
a = b;
右值:只可以出现在“=”的右边,有内存、无名字、临时。
// 变量a是左值,而10、20是立即数,为右值
int a = 10;
a = 20;
// 10 = 20
// 20 = 10
立即数:一个立即数是一块数据存储作为指令本身,而不是在一个中的一部分内容存储器位置或寄存器。(操作系统知识、不理解算了)
常量的类型
常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。
整数常量(尾缀不区分大小写)
进制类型 | 表达方式 |
---|---|
十进制 | 1234 |
无符号十进制 | 1234u |
长十进制 | 1234l |
八进制 | 02322 |
十六进制 | 0x4D2 |
浮点常量(尾缀不区分大小写)
表达类型 | 表达方式 |
---|---|
小数表达 | 3.1415 或 3.1415f |
指数表达 | 31415e-4 |
注意:尾缀表示当前数的类型,其中u(无符号数)、l(长整型)、f(单精度浮点型)、e(10的次幂)等。
字符常量
把字符放在单引号内,例如 'a'
。字符常量里面存在 \
反斜杠时可能会与后面的字符结合发生转移,组合类型可以去查询其他资料。
字符串(字符数据常量)
把字符放在双引号内,例如 "abcd"
。
char myString[] = "Hello World!";
系统会对字符串自动在末尾加’\0’表示字符终止。
常量的定义
使用 #define
预处理器,一般放在程序代码开头或者头文件中。
#define PI 3.1415
#define ENDL '\n'
printf("%f", PI);
printf(ENDL);
使用 const
关键字进行定义,实现语言级别定义的常量。常量必须初始化且不能被修改(理论上,实际上在C语言中可以操纵指针修改)
const int MY_MIN_INT = -1;
运算符、判断、循环
程序的臂膀:运算
算数运算符:+、-、*、/、%、++、–
int a = 10;
int b = 20;
int c = 0;
c = a + b; // 加法
c = a - b; // 减法
c = a * b; // 乘法
c = a / b; // 取整
c = a % b; // 取余
a++; // 后自加
++a; // 前自加(效率高)
关系运算符:==、!=、>、>=、<、<=
int a = 10;
int b = 20;
a == b;
a != b;
a > b;
a >= b;
a < b;
a <= b;
逻辑运算符:&&、||、!
int A = 1;
int B = 0;
A &&B; // 逻辑与,都真才为真
A || B; // 逻辑或,存在真则为真
!(A &&B); // 逻辑非,转换状态
位运算符:&、|、^、~、<<、>>(难点、难理解)参考链接
赋值运算符(赋值运算符、运算符+赋值运算符、位运算符+赋值运算符)
例如:=、+=、&=、<<=等,表达意思即为 左 = 左 操作 右
杂项运算符:sizeof、&、*、三目(三元)运算符
int intSize = sizeof(int); // 返回变量大小
int *ptr = &A; // 返回变量的地址
B = *ptr; // 指向ptr指针存放地址上的变量(指针解引用)
B = (B == 0) ? 0 : 1; // 三元运算符 条件?条件为真的结果:条件为假的结果,表意如下
if (B == 0){B = 0;}else{B = 1;}
运算符优先级
不用刻意记忆,多加使用即可,拿不定的地方用 ()
。
程序的大脑:判断
C语言原生不支持布尔(bool)类型,因此C语言中0表示为false,其余表示为true。
if
语句
if (0) // 判断条件,为真进入语句(作用域),为假不进入语句
{
// 语句……
}
if-else
语句
if (0) // 判断条件,为真进入if语句(作用域),为假进入else语句
{
// 语句……
}
else
{
// 语句……
}
嵌套if
语句
if (0)
{
if (1)
{
// 语句
}
}
switch
语句
一个 switch 语句允许测试一个变量等于多个值时的情况。每个值称为一个case,且被测试的变量会对每个 switch->case 进行检查。
enum DAY // 定义枚举变量
{
MON = 1,
TUE,
WED,
THU,
FRI,
SAT,
SUN
};
enum DAY day = SUN;
switch (day) // 传入测试变量,测试分支的数据类型需要相同
{
case SAT: // 测试分支
printf("REST");
break; // 如果满足此测试分支,完成语句后退出测试,不退出将会继续执行下面的测试
case SUN:
printf("REST");
break;
default: // 默认测试分支
printf("WORK");
}
嵌套switch
语句
使用场景很少,使用逻辑等同于嵌套if语句。
程序的双脚:循环
while
循环
while (0) // 判断为真进入语句,判断为假结束循环
{
// 语句
}
for
循环
int x = 0;
for (int i = 0; i < 2; ++i)
{
// 语句
}
for循环内三个控制量:for ( init ; condition ; increment ) {}
init:最先执行且只执行一次,通常用于初始化循环控制变量,可留白;
condition:控制主体,为真执行循环内语句,为假结束循环,可留白;
increment:增量主体,在每次循环最后执行,一量般用于更新控制变,可留白;
特殊用法——无限循环/死循环
for( ; ; ) {}
do while
循环(不常用)
与while循环相似,但确保循环内语句至少执行一次。
do
{
// 语句
} while (0);
嵌套循环
用法与判断嵌套相似,注意循环控制变量,防止死循环。
// 嵌套for循环
for (int i = 0; i < 10; ++i)
{
for (int j = 0; j < 10; ++j)
{
}
}
// 嵌套while循环
while (0)
{
while (0)
{
}
}
// 互相嵌套
while (1)
{
for (int i = 0; i < 10; ++i)
{
while (0)
{
}
}
}
循环控制语句
break:终止循环或switch语句,继续执行其下一条语句。
while (1)
{
printf("触发死循环\n");
break;
}
continue:终止此次循环,重新开始下次循环。
循环控制经典案例:
int i = 5;
while (i--) // 先赋值(判断)再减减
{
printf("%d\n", i);
if (i % 2 == 0)
{
continue;
}
printf("%d\n", i);
}
i = 5;
while (--i) // 先减减再赋值(判断)
{
printf("%d\n", i);
if (i % 2 == 0)
{
continue;
}
printf("%d\n", i);
}
goto:控制转移(不建议使用)。
函数、作用域、头文件
函数:一组一起执行一个任务的语句。
- 每个 C 程序都至少有一个函数,即主函数 main() ;
- 所有简单的程序都可以定义其他额外的函数;
- 函数还有其他叫法、例如方法、子例程、程序等。
函数的声明
函数的声明告诉编译器函数的名称
、参数列表
、函数返回值类型
,即告诉编译器如何调用函数,其主体可以单独定义。
int max(int a, int b); // 函数声明
注意:函数声明中,参数的名称不重要,类型是必须的。
函数的定义
函数的定义提供了函数的实际主体。
int max(int a, int b) // 返回值类型 函数名(参数列表)
{
// 函数主体
if (a >= b)
{
// 函数返回语句
return a;
}
else
{
// 函数返回语句
return b;
}
}
函数的参数与返回值
传入
如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数
。
形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁。
返回
返回一个形参时,会在主函数栈
上临时
在创建一个临时变量用以返回值的其他操作,调用函数的语句结束时,该临时变量生命周期结束。
函数的传值方式
经典的两数交换问题
值传递
:该方法把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
void swap_error(int a, int b)
{
int temp = a;
a = b;
b = a;
}
函数调用:
int a = 10;
int b = 20;
swap_error(a, b);
printf("a = %d, b = %d\n", a, b);
结果显而易见,是无法完成两数交换的。
指针传递
:通过指针传递方式,形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作。
// 指针传递
void swap_real(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
函数调用:
int a = 10;
int b = 20;
swap_real(&a, &b);
printf("a = %d, b = %d\n", a, b);
利用指针传递变量的地址,通过指针解引用,进而直接对该指针存放的地址上的变量操作(交换),能够实现两数交换。
作用域
作用域是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。
- 在函数或块内部的局部变量(简而言之就是{}之内的变量)
- 在所有函数外部的全局变量(简而言之就是独立于所有函数体的变量)
- 在形式参数的函数参数定义中(简而言之就是函数的形式参数变量)
在本文的变量部分处已作详细解释。
头文件
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。
有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。
在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。
#include <系统头文件>
#include "用户头文件/系统头文件"
使用双引号则会优先引用用户头文件。
注意:一个文件中某一个头文件只能被引用一次,否则会发生错误。
标准的做法是把整个头文件放在条件编译语句中,如下所示:
#ifndef HEADER_FILE // 包装器可以进行判断是否引用,为真则直接跳过到endif中间的全部内容
#define HEADER_FILE
// 完整的头文件内容
#endif
头文件是在C语言中高频使用的编程文件,能够让程序更加直观简洁,同时利用头文件编译等技术能够将关键的程序代码保护起来,增强代码不可见性。