一、C语言基础知识
入门C语言,看这篇就够了;适合刚入门编程的萌新小白
编程语言(programming language),是用来定义计算机程序的形式语言;它是一种被标准化的交流技巧,用来向计算机发出指令。
计算机语言让程序员能够准确地定义计算机所需要使用的数据,并精确地定义在不同情况下所应当采取的行动;编程语言也俗称 “计算机语言”,种类非常的多,总的来说可以分成机器语言、汇编语言、高级语言三大类。
C语言属于高级语言,最初是用于系统开发工作,Linux操作系统内核就是C语言编写;C语言所产生的代码运行速度与汇编语言编写的代码运行速度几乎一样,所以采用 C语言作为系统开发语言。
C语言有如下特点:
-
可产生高效率的程序
-
具有结构化的控制语句
-
语法限制不太严格,程序设计自由度大
-
可在多种计算机平台上编译,移植性好
-
最广泛被使用的系统程序设计语言之一
-
语言简洁、紧凑,使用方便灵活,易于学习
-
运算符、数据类型丰富,具有现代语言的各种数据结构
-
允许直接访问物理地址,能进行位(bit)操作,可以直接对硬件进行操作
C语言应用场景:
-
应用软件编程
-
数据库编程和数字计算
-
网络服务器端底层开发、编程
-
操作系统开发(Linux内核、Windows内核等)
-
嵌入式行业(Linux驱动、MCU编程等)
-
游戏开发设计(游戏引擎,cocos2d等)
C语言是一门面向过程、抽象化的通用程序设计语言,广泛应用于底层开发。C语言能以简易的方式编译、处理低级存储器;C语言是仅产生少量的机器语言以及不需要任何运行环境支持便能运行的高效率程序设计语言,它为操作系统而生,能更直接地与计算机底层打交道,精巧、灵活、高效;所以C语言是进入编程世界的必修课。
二、快速解读一个C程序
/* 这是一个C语言程序 */
#include <stdio.h> //包含头文件
int main(int argc, char **argv) //主函数开始
{printf("我只是一条C语言语句...\n") ; //调用函数打印 表示一条语句return 0 ; //函数返回 也表示一条语句
} //主函数语法结束
键盘侠开始了一遍熟悉的操作后,最简单的一段C程序就被写了出来,此程序看似精简实则也精简(手动doge),来我们从上往下,从前往后解读:
-
#include <stdio.h>:这是一条预处理命令,它的作用是通知C语言编译系统在对C程序进行正式编译之前需做一些预处理工作
-
int main(int argc, char **argv):C语言唯一的主函数,主函数就是C语言程序执行的唯一入口;main 前面的int是主函数的返回类型,argc和argv是函数的参数(稍复杂点,先有个初步认识,**是指针,可高级了呢,后面会详细介绍),标准的写法是加上它俩,也可以写成int main(){…},编译器也能识别的
-
{}:此处的花括号表示将这个部分代码括起来,是开始和结束的标志,代表一个逻辑代码块;这里是代表函数,C语言的语法规定,必须加上
-
printf():指格式化输出函数,主要功能是向标准输出设备按规定格式输出信息;它是C语言标准库函数,定义于头文件stdio.h,所以我们在第一行包含了stdio.h这个文件
-
return:函数的返回值,根据函数类型的不同,返回的值也是不同的,这里是int,返回整型变量
三、C语言编程规范
-
C代码中所有符号均为英文半角符号,使用中文符号编译会报错
-
括号要成对写,比如:{} [] <> (),记住有前就有后,如果需要删除的话也要成对删除
-
每条可执行语句结束的末尾(不一定是一行,可能多行)需要有分号,在C语言里必须要加,否则编译会不通过
-
函数体内的语句要有明显缩进,这是为了方便阅读代码,如果你全顶行写,我保证看你代码的朋友会用比较激动的语言问候你的,通常以按一下Tab键为一个缩进,表现出层次
-
一个说明或一个语句占一行,例如:包含头文件、一个可执行语句结束(有时候也会很多行,具体现象具体分析)、定义一个宏都需要换行(啥?你不知道换行,很简单,我教你,首先看下能不能找到键盘上的enter按键,再用你尊贵的手手轻轻的敲下enter)
-
编写C语言源代码时,应该多使用注释,这样有助于对代码的理解(其实主要是给自己或者同事看的,最好在关键逻辑部分写好注释,养成好的编程习惯);在C语言中有两种注释方式,一种是以/*开始、以*/结束的块注释(block comment);另一种是以//开始、以换行符结束的单行注释(line comment)
什么?你还不知道哪种写法是规范的,看这里:
四、标识符、变量、赋值
标识符
-
标识符只能是字母(A~Z,a~z)、数字(0~9)、下划线(_)组成的字符串,并且其第一个字符必须是字母或下划线
-
标识符中只能包含大小写英文字母、数字、下划线,不允许出现如“ ! 、@、#、¥、%、^、&、*、(、)、/、?等标点,C语言中的标识符不能使用任何中文字符,包括汉字、中文标点;假如我定义一个变量:"int 长度=10;" 看上去是不是怪怪的,显得我们一点都不专业;这样整:"int length = 0 ;" 一下子就高端大气上档次了,人家一看,不错,小伙子指定练过;使用了中文标识符的程序会编不过
-
标识符不能与c语言的保留字(关键字)或者库函数名相同,比如:"int char = 0 ;" "int printf = 0 ;" 注意噢这些操作是不被允许的,编译不过
-
定义变量和函数名以及宏的时候尽量做到见名知义,比如定义一系列函数:
"int can_sing(void){…}" "int can_dance(void){…}" "int can_rap(void){…}",是不都不消写注释一看就知道这些函数是干嘛的,会唱跳RAP对吧,还好学习了两年半,都会(捂嘴笑);假如我写成这样:"int huitiao(void){…}" "int huichang(void){…}" "int huirap(void){…}",拼音式的,编译时也不会报错,但好像有点不专业,所以大家编程的时候千万不要用拼音,中文拼音式的命名一看就比较业余
-
C语言对大小写敏感,因此,相同的字母的不同大小写是不同的标识符,比如length和Length是不同的标识符(我瞅着这俩长得也不大一样)
变量和赋值
C语言通过变量为程序设计者提供了使用存储器的手段,每个变量代表不同的存储单元,有点难懂?大白话来了,变量就是可以变化的量,而每个变量都会有一个名字(标识符);变量是存放在内存中的,所以在程序中每定义一个变量就占用一点内存(当然一个普通变量占的内存很小很小,尽管使劲造)。
C语言的变量由变量名和变量值组成,C程序在使用变量之前必须先定义变量,定义变量又涉及到数据类型(下面会讲到);看个简单的例子:"int a = 10 ;" "unsigned char retry = 0 ;",a和retry都属于变量,而a是整型,retry是无符号字符型,下图显而易见,它们住在不同房子里面,房子代表的是存储单元。
-
变量定义的一般形式为:数据类型 变量名;
-
变量的赋值分为两种方式:先定义再赋值,定义的时候赋值
-
C语言不允许连续赋值,例如:"int a=b=c=10 ;",这样的操作是不合法的(你这是违法行为,请跟我走一趟)
正确的赋值应该是这样的,直接上代码:
unsigned int inum = 0 ; //定义一个无符号整型inum并赋值为0
int itype = 1 ; //定义一个整型变量itype并赋值为1
char a, b, c; //定义3个字符型变量a b c,不赋值
float f_price = 16.8 ; //定义一个浮点型变量f_price并赋值为16.8a = 10 ; //给变量a赋值10
b = 11 ; //给变量b赋值11
c = 1 ; //给变量c赋值1
五、基本数据类型
C语言中,数据类型可分为:
-
基本数据类型
-
构造数据类型
-
指针类型
-
空类型
不同的数据类型也有不同的存储范围,这些范围可能因编译器而异;以下是32位GCC编译器的范围列表,以及内存要求和格式说明符:
整型是用来表示整数的数据类型,C语言中有四种整型:基本型,短整型,长整型,无符号型;基本型的类型说明符为int,占4个字节;短整型的类型说明符为short int或short,占2个字节;长整型的类型说明符为long int或long,32位编译环境下占4个字节;无符号型的类型说明符为unsigned,表示没有符号位(>=0)的整数。
实型是用来表示小数的数据类型;有两种实型:单精度型和双精度型;单精度型的类型说明符为float,占4个字节,有效数字是7位;双精度型的类型说明符为double,占8个字节,有效数字是15位;实型常量可以用小数型或指数型表示。
字符型是用来存储单个字符的数据类型,它占用一个字节的内存空间,可以表示0~255之间的整数或ASCII码;C语言中有三种字符型,分别是char、signed char和unsigned char,它们的区别在于是否有符号;C语言中的字符型数据可以用单引号括起来,例'a'、'q、'$'等,也可以用ASCII码的数值表示;C语言中没有字符串类型,但是有字符串常量的概念,它是由双引号括起来的一串字符序列,以NUL字节结尾,例如"hello"、"3210"、"a"等。
枚举类型是一种特殊的数据类型,它可以用来定义一组具有离散值的常量;枚举类型可以让程序更简洁,更易读,也可以避免使用魔法数字;要定义一个枚举类型,需要使用enum关键字,后面跟着枚举类型的名称,以及用大括号{}括起来的枚举值列表。
数组类型是一种非基础数据类型,它可以用来存储一个固定大小的相同类型元素的顺序集合;数组的元素可以通过下标访问,下标从0开始,到元素个数减1结束;根据元素的类型,数组可以分为字符数组,整型数组,浮点型数组,指针数组,结构体数组等;字符数组可以用来存储字符串,字符串需要一个结束标志'\0'。
结构体类型是一种复合数据类型,它可以用来将多个相关的变量包装成为一个整体使用;结构体类型的变量可以表示一条记录,例如图书的属性,学生的信息等;结构体类型的变量可以包含不同类型的数据成员,例如基本数据类型,指针类型,甚至是其他结构体类型;结构体类型的定义需要使用struct关键字;结构体类型的变量可以参与各种运算,也可以用printf函数输出,也可以作为函数的参数或返回值。
如果想得到某个类型或某个变量在特定平台上的准确大小,可以使用 sizeof 运算符;表达式 sizeof(type) 得到对象或类型的存储字节大小。
啥?你不知道怎样写,来看下面(有涉及到指针和一些不太能理解的操作,没关系,后面会讲,这里先混个眼熟):
/* 整型 实型 字符型 */
int a = 10; // 定义一个整型变量,名为a,赋值为10
float b = 3.14; // 定义一个单精度实型变量,名为b,赋值为3.14
double c = 2.71828; // 定义一个双精度实型变量,名为c,赋值为2.71828
char d = 'A'; // 定义一个字符型变量,名为d,赋值为字符'A'
char e = 65; // 定义一个字符型变量,名为e,赋值为整数65,对应字符'A'unsigned int a = 10; // 定义一个无符号整型变量,名为a,赋值为10
unsigned char b = 'B'; // 定义一个无符号字符型变量,名为b,赋值为字符'B'
unsigned long c = 1000000; // 定义一个无符号长整型变量,名为c,赋值为1000000
/* 枚举类型 */
/* 分别表示一周的七天,每个枚举值都对应一个整数,从0开始递增 */
enum weekday {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY};
/* 把MONDAY的值设为1,后面的枚举值依次加1,即TUESDAY为2,WEDNESDAY为3,依此类推 */
enum weekday {MONDAY = 1, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY};
/* 声明一个枚举类型的变量,需要在变量名前加上枚举类型的名称 */
enum weekday today;
today = MONDAY; //可以赋值为枚举值
/* 可以参与整数运算,也可以用printf函数输出 */
printf("%d\n", today + 1); // 输出2/* 数组类型 */
int a[10]; // 定义一个元素类型为int,元素个数为10的数组
a[0] = 1; // 给数组的第一个元素赋值为1
a[9] = 10; // 给数组的最后一个元素赋值为10
printf("%d\n", a[0] + a[9]); // 输出11
char s[6] = "hello"; // 定义一个字符数组,存储字符串"hello",最后一个元素为'\0'
/* 结构体类型 */
struct book // 定义一个结构体类型,名为book
{char title[50]; // 定义一个字符数组类型的数据成员,名为titlechar author[50]; // 定义一个字符数组类型的数据成员,名为authorint id; // 定义一个整型类型的数据成员,名为id
};/* 结构体类型的变量可以在定义时初始化,也可以在定义后赋值,也可以通过指针访问 */
struct book b1 = {"C语言程序设计", "谭浩强", 1}; // 定义并初始化一个结构体类型的变量,名为b1
struct book b2; // 定义一个结构体类型的变量,名为b2
b2.title = "C++程序设计"; // 给b2的title数据成员赋值
b2.author = "谭浩强"; // 给b2的author数据成员赋值
b2.id = 2; // 给b2的id数据成员赋值
struct book *p = &b1; // 定义一个结构体指针类型的变量,名为p,指向b1的地址
printf("%s\n", p->title); // 通过指针p访问b1的title数据成员,输出"C语言程序设计"/* 结构体类型的变量可以参与各种运算,也可以用printf函数输出,也可以作为函数的参数或返回值 */
printf("%s\n", b1.title); // 输出b1的title数据成员,输出"C语言程序设计"
printf("%d\n", b1.id + b2.id); // 输出b1和b2的id数据成员的和,输出3
struct book getBook(); // 声明一个函数,返回值类型为结构体类型book
void printBook(struct book b); // 声明一个函数,参数类型为结构体类型book
六、格式化输入/输出函数
C语言格式化输入输出语句是指使用一些特定的函数和格式控制符来实现数据的输入和输出;常用的函数有printf()和scanf(),它们分别用于向标准输出(屏幕)发送格式化输出和从标准输入(键盘)读取并格式化输入。
格式控制符是指一些用于指定数据类型和输出格式的字符,例如%d表示整数,%f表示浮点数,%c表示字符,%s表示字符串等;格式控制符可以和一些修饰符组合,例如%5d表示输出宽度为5的整数,%0.2f表示输出保留两位小数的浮点数,%-10s表示输出左对齐的字符串。
printf函数是一个标准输出函数,可以将内容按用户指定的格式输出到屏幕上,使用前要包含stdio.h头文件;它的函数原型是:int printf(const char *format, …);其中,format是一个格式控制字符串,用于指定输出的格式和内容,可以包含普通字符和格式控制符,普通字符原样输出,格式控制符用于输出对应的变量或常量;…是可变参数列表,用于提供输出的数据,必须和格式控制符一一对应。
scanf函数是一个标准输入函数,可以从键盘读取并格式化输入的数据,使用前要包含stdio.h头文件;它的函数原型是:int scanf(const char *format, …);其中,format是一个格式控制字符串,用于指定输入的格式和内容,可以包含普通字符和格式控制符,普通字符必须和输入的字符完全匹配,格式控制符用于读取对应的数据;…是可变参数列表,用于提供输入的数据的地址,必须和格式控制符一一对应。
看代码,敲一个数据转换程序,可以输入一个十进制数,然后输出它的二进制,八进制和十六进制表示:
#include <stdio.h>
int main(int argc, char **argv)
{int num;printf("请输入一个十进制数:\n");scanf("%d", &num);printf("二进制表示为:%b\n", num);printf("八进制表示为:%o\n", num);printf("十六进制表示为:%x\n", num);return 0;
}
七、C语言常量概念
C语言常量是固定值,在程序执行期间不会改变;常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,还有枚举常量。
常量可以用以下方式定义:
-
直接常量是直接在程序中使用的数值或字符,如 3.14, 'a', "hello", 6, 27, -299等
-
符号常量,用一个标识符来代表一个常量值,如 #define PI 3.14,(注意这里#define是预处理指令,后面不需要加分号)这样在程序中就可以用 PI 来表示 3.14 了
-
const 修饰符,用 const 关键字来修饰一个变量,使其成为一个只读变量,如 const int a = 10,a就不能被修改了(这个有点不好理解,后面会详细介绍,如果修改了编译器会直接报错)
常量的编程应用有以下几个方面:
-
常量可以提高程序的可读性,比如用 PI 来表示圆周率,比用 3.14 更直观
-
常量可以提高程序的可维护性,比如如果要修改某个常量值,只需要修改一处定义,而不需要修改多处使用
-
常量可以提高程序的效率,比如编译器可以对常量进行优化,减少运行时的开销
#include <stdio.h>
#define PI 3.14 //定义符号常量
int main(int argc, char **argv)
{const int a = 10; //定义只读变量printf("a = %d\n", a);//a = 20; //错误,不能修改a的值printf("PI = %f\n", PI);printf("6 + 27 = %d\n", 6 + 27); //使用直接常量return 0 ;
}
八、自动类型转换和强制类型转换
自动类型转换是编译器根据代码的上下文环境自行判断的结果,不需要在代码中体现出来; 一般发生在赋值、运算、函数调用等情况,编译器会将较低类型的数据转换为较高类型的数据,以保证精度不丢失。
强制类型转换是程序员(没错,说的就是咱们)明确提出的、需要通过特定格式的代码来指明的一种类型转换;一般用于满足特定的需求,比如截断数据、改变运算结果、转换指针类型等;强制类型转换的格式是:(type_name) expression,其中 type_name 是要转换的目标类型,expression 是要转换的表达式。
当两个不同类型的数据进行运算时,编译器会将较低类型的数据转换为较高类型的数据,然后再进行运算,例如:
int a = 10;
double b = 3.14;
double c = a + b; //自动类型转换,将a转换为double类型后再与b相加
当赋值时,编译器会将右边的表达式转换为左边变量的类型,然后再赋值:
int a = 10;
float b = 3.14;
a = b; //自动类型转换,将b转换为int类型后再赋值给a,会丢失小数部分
当函数调用时(操作确实有些复杂了,但没关系,先认识认识),编译器会将实参转换为形参的类型,然后再传递:
void func(int x)
{printf("x = %d\n", x);
}int main(int argc, char **argv)
{double a = 3.14;func(a); //自动类型转换,将a转换为int类型后再传递给func函数return 0;
}
想要显式地改变变量或表达式的类型时,如下:
int a = 10;
double b = 3.14;
a = (int) b; //强制类型转换,将b转换为int类型后再赋值给a,会丢失小数部分
想要提高运算的精度时,如下:
int a = 10;
int b = 3;
double c = (double) a / b; //强制类型转换,将a转换为double类型后再与b相除,得到更精确的结果
想要进行不同指针类型之间的转换时,如下:
int a = 10;
int *p = &a;
char *q = (char *) p; //强制类型转换,将p转换为char类型的指针后再赋值给q
自动类型转换和强制类型转换综合例子:
#include <stdio.h>
int main(int argc, char **argv)
{int a = 10;double b = 3.14;double c = a + b; //自动类型转换,将a转换为double类型后再与b相加printf("c = %f\n", c);int d = (int) b; //强制类型转换,将b转换为int类型后赋值给d,会丢失小数部分printf("d = %d\n", d);return 0;
}
九、C语言运算符
C语言中的运算符包括以下几种类型:
-
算术运算符:用于进行数值计算,如加减乘除、取余、自增自减等
-
关系运算符:用于进行比较运算,如大于小于、等于不等于、大于等于小于等于等
-
逻辑运算符:用于进行布尔运算,如与或非、短路与短路或等
-
位运算符:用于进行二进制位操作,如按位与按位或按位异或、左移右移、取反等。
-
赋值运算符:用于给变量赋值,如等号、加等减等、左移等右移等等
-
杂项运算符:用于完成一些特殊的功能,如条件运算符、逗号运算符、取地址运算符、间接访问运算符、sizeof运算符等
运算符的结合性和优先级
C语言一共有15个优先级,从高到低依次是:括号、单目运算符、乘除余、加减、移位、关系运算符、相等运算符、按位与、按位异或、按位或、逻辑与、逻辑或、条件运算符、赋值运算符和逗号。
C语言的结合性是指当一个表达式中有相同优先级的运算符时,计算的顺序是从左到右还是从右到左;除了单目运算符(如++,–,!等)、条件运算符(?:)和赋值运算符(=,+=,-=等)是从右向左结合的之外,其余的都是从左向右结合的。
没整明白?上图:
算术运算符
-
+ (加):用于计算两个数值的和,如3 + 5 = 8
-
– (减):用于计算两个数值的差,如7 – 2 = 5
-
* (乘):用于计算两个数值的积,如4 * 6 = 24
-
/ (除):用于计算两个数值的商,如10 / 2 = 5
-
% (取余,模运算):用于计算两个数值的余数,如20 % 8 = 4,注意两个数值必须是整数
-
++ (自增):用于将一个数值加1,如a = 5,a++后,a = 6,可以分为前缀自增和后缀自增,前缀自增先加后用,后缀自增先用后加
-
— (自减):用于将一个数值减1,如b = 10,b–后,b = 9,可以分为前缀自减和后缀自减,前缀自减先减后用,后缀自减先用后减
算术运算符可以用于构成表达式,表达式是由运算符和操作数组成的,可以计算出一个值,如a + b * c是一个表达式,它的值取决于a,b,c的值和运算符的优先级和结合性。
使用算术运算符的编程实例:
#include <stdio.h>
int main(int argc, char **argv)
{int a = 3, b = 5;int c = a + b; // c = 8int d = a - b; // d = -2int e = a * b; // e = 15int f = a / b; // f = 0int g = a % b; // g = 3double h = (double)a / b; // h = 0.6printf("c = %d, d = %d, e = %d, f = %d, g = %d, h = %f\n", c, d, e, f, g, h);return 0;
}
关系运算符
-
> (大于):用于判断两个数值的大小关系,如果左边的数值大于右边的数值,返回1,否则返回0,如5 > 3返回1,2 > 4返回0
-
>= (大于等于):用于判断两个数值的大小关系,如果左边的数值大于或等于右边的数值,返回1,否则返回0,如6 >= 4返回1,3 >= 5返回0,4 >= 4返回1
-
< (小于):用于判断两个数值的大小关系,如果左边的数值小于右边的数值,返回1,否则返回0,如2 < 5返回1,4 < 3返回0
-
<= (小于等于):用于判断两个数值的大小关系,如果左边的数值小于或等于右边的数值,返回1,否则返回0,如3 <= 6返回1,5 <= 3返回0,4 <= 4返回1
-
== (等于):用于判断两个数值是否相等,如果相等,返回1,否则返回0,如4 == 4返回1,3 == 5返回0,注意等于运算符是双等号,不要和赋值运算符单等号混淆
-
!= (不等于 ):用于判断两个数值是否不相等,如果不相等,返回1,否则返回0,如3 != 5返回1,4 != 4返回0
关系运算符可以用于构成关系表达式,关系表达式是由关系运算符和操作数组成的,可以判断出一个真假值,如a > b是一个关系表达式,它的值取决于a,b的值和关系运算符的结果,如果a > b成立,值为1,否则值为0。关系表达式常用于条件判断和循环控制中,如if (a > b) { … },while (a != b) { … }。
使用关系运算符的编程实例代码:
#include <stdio.h>
int main(int argc, char **argv)
{int a = 3, b = 5;int c = a > b; // c = 0int d = a < b; // d = 1int e = a >= b; // e = 0int f = a <= b; // f = 1int g = a == b; // g = 0int h = a != b; // h = 1printf("c = %d, d = %d, e = %d, f = %d, g = %d, h = %d\n", c, d, e, f, g, h);return 0;
}
逻辑运算符
-
|| (或):用于判断两个逻辑表达式的真假关系,如果左边或右边的表达式为真,返回1,否则返回0,如1 || 0返回1,0 || 0返回0
-
&& (且):用于判断两个逻辑表达式的真假关系,如果左边和右边的表达式都为真,返回1,否则返回0,如1 && 1返回1,1 && 0返回0
-
! (非):用于取反一个逻辑表达式的真假值,如果表达式为真,返回0,如果表达式为假,返回1,如!1返回0,!0返回1
逻辑运算符可以用于构成逻辑表达式,逻辑表达式是由逻辑运算符和操作数组成的,可以判断出一个真假值,如a || b是一个逻辑表达式,它的值取决于a,b的值和逻辑运算符的结果,如果a || b成立,值为1,否则值为0;逻辑表达式常用于条件判断和循环控制中,如if (a || b) { … },while (!a) { … }。
使用逻辑运算符的编程实例代码:
#include <stdio.h>
int main(int argc, char **argv)
{int a = 3, b = 5;int c = a > b && b > a; // c = 0int d = a < b || b < a; // d = 1int e = !(a == b); // e = 1printf("c = %d, d = %d, e = %d\n", c, d, e);return 0;
}
位运算符
-
& (按位与):用于对两个数值类型的二进制位进行逐位与运算,如果两个位都为1,返回1,否则返回0,如3 & 5返回1,因为3的二进制是0011,5的二进制是0101,0011 & 0101 = 0001
-
| (按位或):用于对两个数值类型的二进制位进行逐位或运算,如果两个位有一个为1,返回1,否则返回0,如3 | 5返回7,因为3的二进制是0011,5的二进制是0101,0011 | 0101 = 0111
-
^ (按位异或):用于对两个数值类型的二进制位进行逐位异或运算,如果两个位不同,返回1,否则返回0,如3 ^ 5返回6,因为3的二进制是0011,5的二进制是0101,0011 ^ 0101 = 0110
-
~ (按位取反):用于对一个数值类型的二进制位进行逐位取反运算,如果位为1,返回0,如果位为0,返回1,如~3返回-4,因为3的二进制是0011,~3的二进制是1100,按补码规则,1100的原码是1011,即-4。
-
<< (左移):用于将一个数值类型的二进制位按指定的位数向左移动,移出的位被丢弃,右边的空位补0,如3 << 2返回12,因为3的二进制是0011,3 << 2的二进制是1100,即12
-
>> (右移):用于将一个数值类型的二进制位按指定的位数向右移动,移出的位被丢弃,左边的空位根据符号位补0或1,如-3 >> 2返回-1,因为-3的二进制是1011,-3 >> 2的二进制是1111,即-1
位运算符可以用于对数值类型的二进制位进行操作,可以实现一些高效的算法,如位图,加密,压缩。
使用位运算符的编程实例代码:
#include <stdio.h>
int main(int argc, char **argv)
{unsigned char a = 18, b = 12;printf("a << 2 = %d\n", a << 2); //输出72printf("a >> 2 = %d\n", a >> 2); //输出4printf("~a = %d\n", ~a); //输出237printf("a & b = %d\n", a & b); //输出0printf("a | b = %d\n", a | b); //输出30printf("a ^ b = %d\n", a ^ b); //输出30return 0;
}
赋值和杂项运算符
赋值运算符是用来给变量赋值的运算符,有简单赋值(=)和复合赋值(+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=)两种;简单赋值就是把右边的表达式的值赋给左边的变量;复合赋值就是把左边的变量与右边的表达式按照指定的运算符进行计算,并把结果赋给左边的变量。
杂项运算符是指一些不属于其他类别的运算符,有三元条件运算符(?:)、逗号运算符(,)、取地址运算符(&)和间接访问运算符(*);三元条件运算符是用来根据一个条件表达式来选择两个表达式中的一个执行;逗号运算符是用来连接两个或多个表达式,并返回最后一个表达式的值;取地址运算符是用来获取一个变量或对象在内存中的地址;间接访问运算符是用来通过一个指针变量来访问它所指向的变量或对象。
sizeof是一个关键字,不是一个函数;它可以用来获取一个类型或对象在内存中占用的字节数;它可以接收一个类型名或一个表达式作为参数,并返回一个无符号整数;sizeof对于确定数组长度、分配动态内存、检查数据类型等操作很有用。
直接看代码,这里有些赋值和杂项运算符的栗子:
#include <stdio.h>int main(int argc, char **argv)
{// 赋值运算符的例子int a = 10; // 简单赋值printf("a = %d\n", a);a += 5; // 复合赋值,相当于 a = a + 5;printf("a = %d\n", a);// 杂项运算符的例子int b = (a > 10) ? 1 : 0; // 三元条件运算符,相当于 if (a > 10) b = 1; else b = 0;printf("b = %d\n", b);int c = (a++, ++b); // 逗号运算符,相当于 a++; c = ++b;printf("c = %d\n", c);int *p = &a; // 取地址运算符,把变量a的地址赋给指针pprintf("p = %p\n", p);*p = 20; // 间接访问运算符,把指针p所指向的变量(即a)赋值为20printf("*p = %d\n", *p);// sizeof的例子int d[10]; // 定义一个长度为10的整型数组printf("sizeof(int) = %lu\n", sizeof(int)); // 输出一个int类型占用的字节数printf("sizeof(d) = %lu\n", sizeof(d)); // 输出数组d占用的字节数printf("sizeof(d[0]) = %lu\n", sizeof(d[0])); // 输出数组d中第一个元素占用的字节数}
十、C语言流程控制
C语言的分支结构是指程序根据条件有选择地执行不同的语句或代码块的结构;C语言的分支结构主要有两种:if语句和switch语句。
分支结构
if语句是最常见的分支结构,它可以根据一个或多个条件表达式来选择执行不同代码段;if语句有以下几种形式:
// 单分支
if (条件表达式) {语句块1; // 如果条件表达式为真,执行这个语句块
}// 双分支
if (条件表达式) {语句块1; // 如果条件表达式为真,执行这个语句块
} else {语句块2; // 如果条件表达式为假,执行这个语句块
}// 多分支
if (条件表达式1) {语句块1; // 如果条件表达式1为真,执行这个语句块,并跳过后面的else if和else
} else if (条件表达式2) {语句块2; // 如果条件表达式1为假,且条件表达式2为真,执行这个语句块,并跳过后面的else if和else
} else if (条件表达式3) {语句块3; // 如果前面的所有条件表达式都为假,且条件表达式3为真,执行这个语句块,并跳过后面的else if和else
} ...
else {语句块n; // 如果前面的所有条件表达式都为假,执行这个默认的语句块
}
有一种稍微复杂一点并有意思操作:嵌套if-else语句;嵌套if-else意思是在if-else语句内再写if-else语句,如下实例:
#include <stdio.h>int main(int argc, char **argv)
{// 输入一个成绩,判断其等级int score;printf("请输入一个成绩:");scanf("%d", &score);if (score >= 0 && score <= 100) { // 判断成绩是否在合法范围内if (score >= 90) { // 判断成绩是否为优秀printf("优秀\n");} else if (score >= 80) { // 判断成绩是否为良好printf("良好\n");} else if (score >= 60) { // 判断成绩是否为及格printf("及格\n");} else { // 其他情况为不及格printf("不及格\n");}} else { // 成绩不在合法范围内,输出错误信息printf("输入错误\n");}
}
switch语句是一种多路分支结构,它可以根据一个整型或字符型变量或常量的值来选择执行不同的代码段;switch语句有以下形式:
switch (变量或常量) {case 值1:代码段1; // 如果变量或常量等于值1,执行这个代码段,并跳出switch结构(除非使用break)break;case 值2:代码段2; // 如果变量或常量等于值2,执行这个代码段,并跳出switch结构(除非使用break)break;case 值3:代码段3; // 如果变量或常量等于值3,执行这个代码段,并跳出switch结构(除非使用break)break;...default:默认代码段; // 如果变量或常量与所有case中的值都不匹配,执行这个默认的代码段,并跳出switch结构(除非使用break)break;
}
switch语句中可以使用break和continue关键字来控制程序的流程;break关键字用于跳出switch语句,执行switch语句后面的代码;如果不加break,程序会一直往后执行,直到遇到break或者switch语句结束。
continue关键字用于跳过本次循环,开始下一次循环;在switch语句中,continue必须结合循环使用,否则会报错。
default是一个关键字,它只用在switch语句中;用于指定当switch语句中的所有case都不匹配时,所要执行的代码块;default语句可以放在switch语句的任何位置,但通常放在最后,以便清晰地表示默认情况;在default语句后面也需要加break,以便跳出switch语句,否则程序会继续执行后面的case。
有意思的code栗子又来了呢,下面实例充分体现了continue、default、break在switch中的用法:
#include <stdio.h>int main(int argc, char **argv)
{// 输入一个数字,判断其奇偶性int num;printf("请输入一个数字:");scanf("%d", &num);switch (num % 2) {case 0:printf("偶数\n");break; // 跳出switch语句case 1:printf("奇数\n");break; // 跳出switch语句default:printf("输入错误\n");break; // 跳出switch语句}// 输入一个字符,判断其是否为元音字母char ch;printf("请输入一个字符:");scanf("%c", &ch);for (int i = 0; i < 5; i++) { // 循环5次switch (ch) {case 'a':case 'e':case 'i':case 'o':case 'u':printf("元音字母\n");continue; // 跳过本次循环,开始下一次循环default:printf("非元音字母\n");continue; // 跳过本次循环,开始下一次循环}}
}
循环结构
C语言循环结构是指可以重复执行一段代码的语句;C语言提供了三种循环结构,分别是 for 循环、while循环和do while循环;它们的区别在于循环条件的判断位置和执行次数。
for 循环是一个由四个部分组成的语句,分别是初始化、条件、增量和循环体;它可以用来执行固定次数的循环,或者根据某个变量的变化来控制循环。
while 循环是一个由两个部分组成的语句,分别是条件和循环体;它可以用来执行不确定次数的循环,或者根据某个表达式的真假来控制循环;它是一个入口条件循环,也就是说,在每次执行循环体之前都要判断条件是否成立。
do while 循环也是一个由两个部分组成的语句,分别是循环体和条件;它与 while 循环的区别在于,它是一个出口条件循环,也就是说,在每次执行完循环体之后才判断条件是否成立;因此,do while 循环至少会执行一次。
看着描述好像懂了耶(眼睛会了,手不会),来,上操作;
for 循环语法示意:
for (初始化; 条件; 增量) {循环体;
}
for 循环代码例子:
// 计算 1 到 100 的和
#include <stdio.h>
int main()
{int i = 0;int sum = 0;for (i = 1; i <= 100; i++) {sum = sum + i;}printf("sum = %d\n", sum);return 0 ;
}
while 循环语法示意:
while (条件) {循环体;
}
while 循环代码例子:
// 输出从 n 到 m 的所有偶数
#include <stdio.h>
int main()
{int n = 10;int m = 20;while (n <= m) {if (n % 2 == 0) {printf("%d\n", n);}n++;}return 0 ;
}
do while 循环语法示意:
do {循环体;
} while(条件);
do while 循环代码例子:
// 输入一个正整数,输出它的阶乘
#include <stdio.h>
int main()
{int n, i;unsigned long long fact = 1;printf("输入一个整数: ");scanf("%d",&n);// 如果输入为负数,显示错误信息if (n < 0)printf("错误!负数没有阶乘。");else {for(i=1; i<=n; ++i) {fact *= i; // fact = fact*i;}printf("%d 的阶乘为 %llu", n, fact);}return 0;
}
使用for 循环的注意事项:
-
通常适用于循环次数已知的场景
-
for 循环的初始化部分可以是多个赋值语句,用逗号隔开
-
for 循环的条件部分可以是多个表达式,用逻辑运算符连接
-
for 循环的增量部分可以是多个语句,用逗号隔开
-
for 循环中可以使用 break 和 continue 控制循环流程
使用while 循环的注意事项:
-
通常适用于循环次数未知的场景
-
while 循环必须先判断条件是否成立,然后决定是否执行循环体语句
-
while 循环中必须有改变条件表达式的语句,否则会造成死循环
-
while 循环中可以使用 break 和 continue 控制循环流程
使用do while 循环的注意事项:
-
do while 循环至少执行一次循环体
-
do while 循环先执行循环体语句,然后判断条件是否成立,再决定是否继续循环
-
do while 循环中必须有改变条件表达式的语句,否则会造成死循环
-
do while 循 环中可以使用 break 和 continue 控制循环流程
什么?你还不知道在循环结构中使用continue和break?这必须安排上,一下几段代码示意了for和while循环分别使用break和continue:
// 输出 1 到 10,当 i 等于 5 时跳出循环
#include <stdio.h>
int main()
{int i = 0;for (i = 1; i <= 10; i++) {printf("%d\n", i);if (i == 5) {break;}}return 0 ;
}
// 输出从 n 到 m 的所有奇数,跳过偶数
#include <stdio.h>
int main()
{int n = 10;int m = 20;int i = n;for (i = n; i <= m; i++) {if (i % 2 == 0) {continue;}printf("%d\n", i);}return 0 ;
}
// 输入一个正整数,输出它的因子,如果输入负数或零则结束循环
#include <stdio.h>
int main()
{int n, i;while (1) {printf("输入一个正整数: ");scanf("%d",&n);// 如果输入为负数或零,跳出循环if (n <=0) {break;}printf("%d 的因子有: ", n);for(i=1; i<=n; ++i) {if (n % i ==0) { printf("%d ",i);}}printf("\n");}return 0;
}
// 输入一个正整数,判断它是否为素数,如果输入负数或零则跳过判断继续输入
#include <stdio.h>
int main()
{int n, i, flag;while (1) {printf("输入一个正整数: ");scanf("%d",&n);// 如果输入为负数或零,跳过判断继续输入if (n <=0) {continue;}flag =1; // 假设是素数// 判断是否有除了1和自身以外的因子for(i=2; i<n/2; ++i){// 若无余则不是素数if(n%i==0){flag=0;break;}}if(flag==1)printf("%d 是素数。\n",n);elseprintf("%d 不是素数。\n",n);}return 0 ;
}
啥?你还写不出来,来,我把手借给你;多写多练多想,绝对的能行;还想玩点高级的?安排,循环嵌套:
/* while循环嵌套打印9*9乘法口诀表 */
#include <stdio.h>
int main()
{int x = 1; //定义第一个乘数int y = 1; //定义第二个乘数while (x <= 9) //外层循环控制行{y = 1; //每次进入内层循环时,将y重置为1while (y <= x) //内层循环控制列{printf("%d*%d=%d\t", y, x, x * y); //打印乘法式子和结果,\t表示制表符,用来对齐输出y++; //y自增1,继续下一列的打印}printf("\n"); //内层循环结束后,换行输出下一行x++; //x自增1,继续下一行的打印}return 0;
}
/* for循环嵌套打印9*9乘法口诀表 */
#include <stdio.h>
int main()
{int i, j; //定义两个循环变量for (i = 1; i <= 9; i++) //外层循环控制行{for (j = 1; j <= i; j++) //内层循环控制列{printf("%d*%d=%d\t", j, i, i * j); //打印乘法式子和结果,\t表示制表符,用来对齐输出}printf("\n"); //内层循环结束后,换行输出下一行}return 0;
}
再花里胡哨一点的操作:
/* for嵌套while循环打印9*9乘法口诀表 */
#include <stdio.h>
int main()
{int x = 1; //定义第一个乘数int y; //定义第二个乘数for (x = 1; x <= 9; x++) //外层循环控制行{y = 1; //每次进入内层循环时,将y重置为1while (y <= x) //内层循环控制列{printf("%d*%d=%d\t", y, x, x * y); //打印乘法式子和结果,\t表示制表符,用来对齐输出y++; //y自增1,继续下一列的打印}printf("\n"); //内层循环结束后,换行输出下一行}return 0;
}
十一、合理使用goto语句
简单来说,goto语句是一种无条件跳转语句,它可以让程序跳转到同一函数内的某个标记处;它的语法是:
goto label; //跳转到label处
//其他代码
label: //标记位置
但是,goto语句也有很多缺点,比如:
-
它会导致程序逻辑混乱,难以理解和修改
-
它会破坏程序结构,使得代码不符合自顶向下的设计原则
-
它会增加程序出错的风险,比如造成死循环或内存泄漏
因此,在任何编程语言中,都不建议使用goto语句;任何使用goto语句的程序都可以改写成不需要使用goto语句的写法,但在Linux驱动开发中会常见到它。
Linux内核或驱动中大量使用goto语句,主要为了处理错误情况和释放内存空间;例如,下面是一个简单的驱动程序片段,它使用goto语句来跳转到不同的标签处,根据不同的错误码来释放已经分配的资源(这里直接是起飞的操作,涉及到Linux驱动开发代码,先看看,不懂没关系,我来手把手):
static int __init hello_init(void) //定义一个静态的初始化函数
{int ret = -1; //定义一个返回值变量,初始值为-1printk(KERN_INFO "Hello World enter\n"); //打印一条内核信息hello_class = class_create(THIS_MODULE,"hello_class"); //创建一个设备类if(IS_ERR(hello_class)) //判断设备类是否创建成功{ret = PTR_ERR(hello_class); //如果失败,获取错误码并赋值给retgoto class_err; //跳转到class_err标签处,结束函数}hello_device = device_create(hello_class,NULL,MKDEV(HELLO_MAJOR,0),NULL,"hello_device"); //创建一个设备节点if(IS_ERR(hello_device)) //判断设备节点是否创建成功{ret = PTR_ERR(hello_device); //如果失败,获取错误码并赋值给retgoto device_err; //跳转到device_err标签处,释放设备类资源并结束函数}cdev_init(&hello_cdev,&hello_ops); //初始化一个字符设备ret = cdev_add(&hello_cdev,MKDEV(HELLO_MAJOR,0),HELLO_COUNT); //添加字符设备到系统if(ret < 0) //判断字符设备是否添加成功goto cdev_err; //如果失败,跳转到cdev_err标签处,释放设备节点和设备类资源并结束函数return 0; //如果成功,返回0cdev_err: device_destroy(hello_class,MKDEV(HELLO_MAJOR,0)); //释放设备节点资源
device_err:class_destroy(hello_class); //释放设备类资源
class_err:return ret;
}
看不懂有点难?没关系,先看代码结构,别跟细节,因为涉及到很多后续的内容;跟着我一起搞学习,相信屏幕前的你也能写出上面的驱动代码(是不是立马感觉高大上了许多)。
十二、C语言数组
数组概念
C语言数组是一种数据结构,它可以存储固定大小的相同类型的元素的顺序集合;数组的元素可以通过下标访问,下标从0开始;C语言支持一维、二维和多维数组,其中二维数组可以看作是由一维数组组成的数组。
一维数组可以类比为一排连续的房间,每个房间都有一个门牌号,从 0 开始递增。每个房间里可以存放相同类型的物品,例如书籍、衣服等。要访问或修改某个房间里的物品,就需要知道它的门牌号,也就是数组下标。要找到第一个房间的位置,就需要知道它的地址,也就是数组名。
二维数组可以类比为一个楼层,每个楼层有多排多列的房间,每个房间都有一个行号和列号,从 0 开始递增。每个房间里可以存放相同类型的物品,例如家具、电器等;要访问或修改某个房间里的物品,就需要知道它的行号和列号,也就是数组下标。要找到第一个楼层的位置,就需要知道它的地址,也就是数组名。
一维数组
一维数组是一组相同类型的数据集合,每个元素通过数组名和一个下标唯一确定。
在内存中是按照顺序从低地址到高地址存储的,每个元素占用相同大小的空间。
首地址是数组名所代表的地址,也就是第一个元素的地址;可以用指针来访问或修改数组的元素,例如 int *p = a; 表示定义一个指针 p 指向 a 数组的首地址(涉及指针,后面会介绍)。
定义数组:要指定数组的类型、名称和大小,定义数组的一般形式是:类型说明符 数组名[常量表达式];例如 int a[10]; 表示定义一个名为 a 的整型数组,它有 10 个元素
初始化数组:要给数组的元素赋初值,可以在定义时用花括号列出,例如 int a[5] = {1, 2, 3, 4, 5}; 表示定义并初始化一个一维数组;如果没有给出所有元素的初值,剩余的元素会自动为0
使用数组:要通过下标访问或修改数组的元素,下标从 0 开始,例如 a[0] = 10; 表示把 a 数组的第一个元素改为 10;可以用循环遍历数组的所有元素,例如 for (int i = 0; i < 5; i++) printf("%d ", a[i]); 表示打印出 a 数组的所有元素
操作数组:要对数组进行排序、查找、插入、删除等操作,需要编写相应的函数或算法,例如冒泡排序、二分查找等;也可以用 C 标准库提供的函数来操作字符串(字符数组),例如 strcpy、strcat、strcmp 等
二维数组
多维数组是元素为数组的数组,可以用一个数组名和多个下标来访问或修改每个元素.
定义多维数组的一般形式是:类型说明符 数组名[常量表达式1][常量表达式2]…[常量表达式n];,其中类型说明符表示数组元素的类型,常量表达式表示每一维的长度;例如:int c[2][3][4]; 表示定义一个三维整型数组 c,它有 2 个二维数组,每个二维数组有 3 个一维数组,每个一维数组有 4 个整数。
初始化多维数组时,可以用花括号包含多层值来初始化每一维的元;例如:int d[2][3] = {{1, 2, 3}, {4, 5, 6}}; 表示定义并初始化一个二维整型数组 d,它有两个一维数组,分别为 {1, 2, 3} 和 {4, 5, 6}。
访问或修改多维数组元素时,需要用到数组名和多个下标,每个下标从 0 开始到对应长度减 1 结束;例如:printf("%d\n", d[0][1]); 表示打印出 d 数组的第一个一维数组(下标为 0)的第二个元素(下标为2)。
字符串与数组
字符串是由一系列字符组成的,通常用双引号包围;例如:"Hello" 是一个字符串,它由 5 个字符 H, e, l, l 和 o 组成。
字符数组是一种特殊的数组,它的元素都是字符类型;例如:char a[10]; 表示定义一个长度为 10 的字符数组 a,它可以存放 10 个字符。
C语言中没有专门的字符串类型,所以通常用字符数组来表示和存储字符串;例如:char b[6] = "Hello"; 表示定义并初始化一个长度为 6 的字符数组 b,它存放了字符串 "Hello\0"('\0'是字符串结束标记)。
字符串必须以'\0'结束,这个 '\0' 表示字符串的结束标志,也占用一个字节的空间;所以定义字符数组时,要留出足够的空间来存放 '\0'。例如:char c[6] = "Hello"; 中 c 数组的最后一个元素就是 '\0'。
字符串可以直接赋值给字符指针,但不能直接赋值给字符数组。例如:char *d = "Hello"; 是正确的,但 char e[6]; e = "Hello"; 是错误的(又是指针,可见,C语言中指针很重要)。
字符指针指向的字符串常量是不可修改的,但字符数组中的元素是可以修改的。例如:d[0] = 'h'; 是错误的,但 e[0] = 'h'; 是正确的。
C语言中有很多常用的字符串函数,可以对字符串进行各种操作。介绍一些常见的字符串函数:
-
strlen:返回字符串的长度,不包括结束符'\0'
-
strcpy:把一个字符串复制到另一个字符串,包括结束符'\0'
-
strcmp:比较两个字符串的大小,如果相等返回0,如果第一个字符串大于第二个返回正数,否则返回负数。
-
strcat:把一个字符串追加到另一个字符串的末尾,覆盖原来的结束符'\0'
-
strstr:查找一个子串在一个主串中第一次出现的位置,并返回指向该位置的指针。如果没有找到,则返回NULL
-
strtok:把一个字符串按照指定的分隔符分割成若干个子串,并依次返回每个子串的指针。如果没有更多子串,则返回NULL
这些函数都需要包含头文件string.h才能使用。
数组应用-冒泡排序
数组的常规操作如下:
int a[10]; // 声明了一个整型数组,包含10个元素
char b[20]; // 声明了一个字符数组,包含20个元素
double c[5]; // 声明了一个双精度浮点数数组,包含5个元素a[0] = 1; // 给数组a的第一个元素赋值为1
b[19] = 'z'; // 给数组b的最后一个元素赋值为'z'
c[2] = 3.14; // 给数组c的第三个元素赋值为3.14// 获取数组a的长度,并打印结果
int len_a = sizeof(a) / sizeof(a[0]);
printf("The length of array a is %d\n", len_a);// 获取数组b的长度,并打印结果
int len_b = sizeof(b) / sizeof(b[0]);
printf("The length of array b is %d\n", len_b);
// 使用for循环遍历数组a,并打印每个元素的值
for (int i = 0; i < 10; i++) {printf("%d ", a[i]);
}
printf("\n");// 使用while循环遍历数组b,并打印每个元素的值
int j = 0;
while (j < 20) {printf("%c ", b[j]);j++;
}
printf("\n");
利用数组实现冒泡排序:
//冒泡排序函数
void bubble_sort(int num[],int n) //存放要排序数的数组,要排序数的个数
{int i,j,t; //i,j为遍历变量,t为临时交换变量for(i=0;i<n-1;i++) //控制内层循环次数{//控制每次循环最小的比较次数for(j=0;j<n-1-i;j++) //这里-i是因为每次冒完泡出来的那个数位置固定了不用管{//比较相邻的两个数if(num[j]>num[j+1]) //把大的数冒泡到后面,即从小到大排序{//把第一个数与第二个数交换位置t=num[j+1];num[j+1]=num[j];num[j]=t;}}}
}
注意事项
定义数组时,数组的长度必须是常量、常量表达式或宏定义,不能是变量;例如:int n = 10; int a[n]; 这样是错误的,应该改为 #define N 10; int a[N]; 或 const int n = 10; int a[n];。
初始化数组时,如果花括号内提供的初始值个数少于数组长度,那么剩余的元素会自动初始化为0;例如:int b[5] = {1, 2}; 表示 b 数组的前两个元素为 1 和 2,后三个元素为 0。
访问或修改数组元素时,不能越界,即下标不能小于 0 或大于等于数组长度;例如:int c[3] = {1, 2, 3}; printf("%d\n", c[-1]); 是错误的,因为 -1 越界了。
数组名代表数组首元素的地址,不能被赋值或修改;例如:int d[4] = {4, 5, 6, 7}; d = d + 1; 是错误的,因为 d 是一个常量指针。
十三、C语言函数
函数是一组一起执行一个任务的语句;函数可以接收用户传递的参数,也可以不接收;函数可以返回一个值,也可以不返回;将代码段封装成函数的过程叫做函数定义。
C语言有很多自带的库函数,例如数学函数,字符串函数,输入输出函数等,也可以自己定义函数(用户自定义函数)。
C语言函数的概念是:函数是一组一起执行一个任务的语句;一个函数里面可以调用 n 个函数,即大函数调用小函数,小函数又调用“小小”函数。
自定义函数
C语言函数的构成包括:返回值类型,函数名,参数列表和函数体;函数的定义规则是:函数名必须以字母或下划线开头,不能与关键字重名,不能与其他函数或变量同名;函数的返回值类型和参数类型必须明确指定;函数体必须用大括号包围;下面是一个用户自定义函数:
int max(int a, int b) // 返回值类型为int,函数名为max,参数列表为(int a, int b)
{if (a > b) // 函数体return a;elsereturn b;
}
C语言函数的参数是指在调用函数时传递给函数的数据;参数分为形参和实参两种,形参(形式参数)是指在函数定义中出现的参数,它没有数据,只能等到函数被调用时接收传递进来的数据,例如:
int max(int a, int b) // a和b就是形参
{// 函数体
}
实参(实际参数)是指在函数调用中出现的参数,它有具体的值,可以是常量、变量或表达式;例如:
int x = 10, y = 20; // 定义两个变量x和y
int z = max(x, y); // x和y就是实参
当调用一个有参数的函数时,会将实参的值拷贝给形参指向的内存空间(又涉及到指针);这样,形参就可以在函数体中使用实参的值来完成任务,函数的传参概念十分重要,传递的是值;C语言函数的调用是指在程序中使用已经定义好的函数来完成某个功能。
有参函数是指在主调函数调用被调函数时,主调函数通过参数向被调函数传递数据,如上代码所示就是有参函数。
无参函数是指在主调函数调用被调函数时,主调函数不需要向被调函数传递数据;例如:
void hello() // 定义一个无参函数,没有参数
{printf("Hello world!\n"); // 函数体
}
hello(); // 调用无参函数,不需要传递参数
有参函数和无参函数不能单单凭有无返回值确定为哪类函数,最核心本质就是主调函数需不需要向被调函数传递数据。
函数返回值是指在执行完一个有返回值的函数后,该值会返回给主调方使用;返回值的类型必须与定义时的类型相同或兼容(自动/强制类型转换):
int add(int a, int b) // 定义一个有返回值的函数,返回值类型为int
{return a + b; // 返回a和b的和作为返回值
}int x = 10, y = 20; // 定义两个变量x和y
int z = add(x, y); // 调用有返回值的函数,并将返回值赋给z
printf("The sum is %d\n", z); // 输出结果void hello() // 定义一个无返回值的函数,没有return语句
{printf("Hello world!\n");
}hello(); // 调用无返回值的函数,不需要接收任何返回值
C语言中的return是一个关键字,它用于在函数中返回一个值或者终止函数的执行,例如:
int add(int a, int b) // 定义一个有返回值的函数,返回值类型为int
{return a + b; // 返回a和b的和作为返回值
}void hello() // 定义一个无返回值的函数,没有return语句
{printf("Hello world!\n");
}int max(int a, int b) // 定义一个有返回值的函数,返回值类型为int
{if (a > b) // 如果a大于breturn a; // 返回a作为最大值else // 否则return b; // 返回b作为最大值
}
在上面的例子中,add函数使用了return语句来将a和b的和作为返回值传递给主调方;hello函数没有使用return语句,当然它也可以使用return;(不接参数表示直接返回)因为它不需要返回任何值;max函数使用了多个return语句来根据条件提前结束函数并返回最大值。
当执行到return语句时,会将return后面的表达式(如果有)的结果赋给主调方指定的变量(如果有),然后退出当前函数并回到主调方继续执行;例如:
int x = 10, y = 20; // 定义两个变量x和yint z = add(x, y); // 调用add函数,并将x和y作为参数传递给它,将返回值赋给zprintf("The sum is %d\n", z); // 输出结果hello(); // 调用hello函数,不需要传递参数或接收返回值printf("The max is %d\n", max(x, y)); // 调用max函数,并将x和y作为参数传递给它,直接输出其返回值
递归函数
C语言递归函数是指一个函数在其函数体内调用自身的函数;递归函数可以用来解决一些数学问题,如阶乘、斐波那契数列等;递归函数的优点是可以简化代码量,缺点是运行效率较低。
递归函数使用场景有很多,比如树形菜单,快速排序,汉诺塔问题等。这些场景都是可以把问题分解为相同或相似的子问题,然后用同一个函数来解决。
递归实现阶乘:
定义一个函数fac,接受一个整数n作为参数。
如果n小于等于1,那么返回1
否则,返回n乘以fac(n-1)
//定义阶乘函数
int fac(int n)
{if (n <= 1) { //基准情况return 1;} else { //一般情况return n*fac(n - 1); //调用自身}
}
再来个复杂一点的例子,如快速排序:
//快速排序
void quicksort(int a[], int left, int right)
{if (left < right) {int i = left;int j = right;int pivot = a[left]; //选取第一个元素作为基准while (i < j) {while (i < j && a[j] >= pivot) //从右向左找到第一个小于基准的元素j--;if (i < j)a[i++] = a[j]; //将该元素放到左边while (i < j && a[i] <= pivot) //从左向右找到第一个大于基准的元素i++;if (i < j)a[j--] = a[i]; //将该元素放到右边}a[i] = pivot; //将基准放到中间位置quicksort(a, left, i - 1); //对左半部分递归排序quicksort(a, i + 1, right); //对右半部分递归排序}
}
简单算法实例:
有5个人坐在一起,问第5个人多少岁?他说比第4个人大2岁。问第4个人岁数,他说比第3个人大2岁。问第3个人,又说比第2人大两岁。问第2个人,说比第1个人大两岁。最后 问第1个人,他说是10岁。请问第5个人多大?
#include <stdio.h>
//定义年龄函数
int age(int n)
{if (n == 1) { //基准情况return 10;} else { //一般情况return age(n - 1) + 2; //调用自身}
}int main()
{printf("第5个人的年龄是%d岁", age(5)); return 0;
}
静态/外部函数
函数的声明和定义可以用static或extern修饰符来指定函数的存储类别。
static修饰符表示函数是内部的,也就是说它只能被本文件中其他函数所调用,它的作用域只局限于所在文件;在定义内部函数时,在函数名和函数类型的前面加static,即:
static 类型名 函数名(形参表);
使用内部函数,可以使函数和外部变量放在文件的开头,前面都加static使之局部化,表示其他文件不能访问。
extern修饰符表示函数是外部的,也就是说它可以被其他文件中的函数所调用,它的作用域不受文件限制;在定义外部函数时,在函数名和函数类型的前面加extern,即:
extern 类型名 函数名(形参表);
使用外部函数,可以使多个文件共享同一个功能模块;以下是内部函数和外部函数的区别:
file1.c
// file1.c#include <stdio.h>// 内部静态变量
static int a = 10;// 外部非静态变量
int b = 20;// 内部静态(默认)函数
static void func1()
{printf("file1: func1: a = %d, b = %d\n", a, b);
}// 外部非静态(默认)函数
void func2()
{printf("file1: func2: a = %d, b = %d\n", a, b);
}int main()
{func1();func2();return 0;
}
file2.c
// file2.c#include <stdio.h>// 引用外部非静态变量
extern int b;// 引用外部非静态(默认)函数
void func2();int main()
{// 调用外部非静态(默认)变量和函数printf("file2: main: b = %d\n", b);func2();// 不能调用内部静态(默认)变量和函数// printf("file2: main: a = %d\n", a); // 错误:未声明标识符“a”// func1(); // 错误:未声明标识符“func1”return 0;
}
输出结果:
file1: func1: a = 10, b = 20
file1: func2: a = 10, b = 20file2: main: b = 20
file1: func2: a = 10, b = 20
在不同的文件中,内部静态(默认)变量和函数只能被本文件中其他变量或者函调用;而外部非静态(默认)变量和函则可以被其他文件中其他变量或者函调用。
十四、变量存储类型
C语言变量的存储类别决定了变量的作用域和生存周期;根据存储类别,变量可以分为以下几种:
-
局部变量:在函数内部定义的变量,只能在该函数内部使用,每次函数调用时创建,函数结束时销毁;局部变量可以是自动的(auto)、静态的(static)或寄存器的(register)
-
全局变量:在函数外部定义的变量,可以在整个程序文件中使用,程序开始时创建,程序结束时销毁;全局变量可以是外部的(extern)或静态的(static)
光说不练可不行,来上陈年老代码:
#include <stdio.h>
// 全局变量
int x = 10;
int y = 20;void func1()
{// 局部变量int x = 100;printf("func1: x = %d, y = %d\n", x, y);
}void func2()
{// 局部变量int y = 200;printf("func2: x = %d, y = %d\n", x, y);
}int main()
{printf("main: x = %d, y = %d\n", x, y);func1();func2();return 0;
}
/* 输出结果 */
//main: x = 10, y = 20
//func1: x = 100, y = 20
//func2: x = 10, y = 200
变量存储类型关键字:
-
auto:表示自动存储类别,默认情况下所有局部变量都是auto类型(默认就是可省略在变量前加auto修饰);auto类型的局部变量存在于栈上,每次函数调用时分配空间,函数结束时释放空间
-
static:表示静态存储类别,可以修饰局部变量或全局变量;static类型的局部变量存在于静态存储区,在程序运行期间一直存在,只能在定义它的函数内访问;static类型的全局变量也存在于静态存储区,在程序运行期间一直存在,在定义它的文件内可访问
-
register:表示寄存器存储类别,只能修饰局部变量;register类型的局部变量存在于寄存器中,访问速度最快,但数量有限;register类型只是一个建议,并不保证一定分配到寄存器中
-
extern:表示外部链接存储类别,默认情况下所有全局变量都是extern类型;extern类型的全局变量存在于静态存储区,在程序运行期间一直存在,并且可以被其他文件引用和访问;extern也可以用来声明一个在其他文件中已经定义好的全局变量
继续,上实例:
#include <stdio.h>// 全局静态变量
static int a = 10;// 全局非静态变量
int b = 20;// 外部函数声明
extern void func();int main()
{// 局部自动(默认)变量auto int c = 30;// 局部寄存器(请求)变量register int d = 40;// 局部静态(改变)变量static int e = 50;printf("main: a = %d, b = %d, c = %d, d = %d, e = %d\n", a, b, c, d, e);func();return 0;
}void func()
{// 引用全局非静态(默认)变量extern int b;// 定义全局非静态(默认)变量int f = 60;printf("func: b = %d, f = %d\n", b, f);
}
输出结果
main: a = 10, b = 20, c = 30, d = 40, e = 50
func: b = 20, f = 60
上面代码表示了在不同的文件和函数中,不同存储类别修饰符影响了变量和函数的作用域和生命周期。
十五、令人费解的指针
C语言中,指针是一种特殊的数据类型,它可以存储一个变量的地址,也就是变量在内存中的位置;通过指针,我们可以直接操作内存中的数据,而不需要知道变量的名字。
变量、内存、指针联系
我们要知道内存是什么;内存是计算机中用来存储数据和指令的硬件设备,它由许多个最小的存储单元组成,每个单元都有一个唯一的编号,称为地址;我们可以把内存想象成一个巨大的柜子,每个抽屉都有一个标签,里面可以放东西
其次,我们要知道变量是什么;变量是程序中用来表示数据的标识符,它有一个类型(如int、char等),一个名字(如a、b等),和一个值(如10、'c’等);当我们定义一个变量时,就相当于在内存中申请了一块空间来存放它的值,并且给这块空间起了一个名字;我们可以把变量想象成柜子里的抽屉上贴着名字的便利贴,并且里面放着东西。
最后,我们要知道指针是什么;指针是一种特殊的变量,它也有类型、名字和值,但它的值不是普通的数据,而是另一个变量或对象(如数组、函数等)所在内存单元的地址;当我们定义一个指针时,就相当于在内存中申请了一块空间来存放它所指向对象的地址,并且给这块空间起了一个名字;我们可以把指针想象成柜子里另外一种颜色的便利贴,并且上面写着另外一张便利贴所在抽屉的标签号码。
指针基础概念
指针的基本概念有以下几点:
-
指针变量的定义:类型 *指针名;例如:int *p;
-
指针变量的赋值:指针名 = &变量名;例如:p = &a;表示把变量a的地址赋给指针p
-
指针变量的解引用:*指针名;例如:*p;表示访问指针p所指向的内存单元中存储的数据
-
指针变量的运算:指针可以进行加减运算,但要注意运算结果仍然是一个地址,而且与指针所指向的类型有关;例如:p + 1;表示把p所指向的地址加上一个整型数据所占用的字节数(通常为4个字节)
-
指针作为函数参数:函数可以接收一个或多个指针作为参数,这样可以实现在函数内部修改函数外部的变量或者传递大型数据结构(如数组)时提高效率
使用指针有以下几个意义:
-
实现函数之间共享数据或者返回多个值
-
提高传参效率和节省内存空间
-
动态分配和管理堆内存
-
实现复杂的数据结构(如链表、树、图等)
指针的内存模型
指针的内存模型是指指针变量在内存中的存储方式和表示方法。
一般来说,内存可以分为四个区域:代码区、数据区、堆区和栈区;代码区存放程序的指令,数据区存放全局变量和静态变量,堆区存放动态分配的内存,栈区存放局部变量和函数调用时的参数。
指针变量本身也是一种变量,它占用一定的内存空间(通常为4个字节或8个字节),并且有一个地址;指针变量中存储的值是另一个变量的地址,也就是说,指针变量指向了另一个变量所在的内存单元。
例如:
int a = 10; // 定义一个整型变量a
int *p = &a; // 定义一个整型指针p,并把a的地址赋给它
假设a占用4个字节,p占用8个字节(64位编译环境下),在内存中可以表示为:
地址 |
值 |
1000 |
10 |
1004 |
… |
… |
… |
2000 |
1000 |
2008 |
… |
其中1000是a所在的地址,10是a的值;2000是p所在的地址,1000是p的值(也就是a的地址);2008是下一个内存单元的地址。
我们可以通过*p来访问或修改a所在内存单元中的值;例如:
printf("%d\n", *p); // 输出10
*p = 20; // 修改a为20
printf("%d\n", a); // 输出20
我们还可以通过p来改变它所指向的对象,例如:
int b = 30; // 定义另一个整型变量b
p = &b; // 把b的地址赋给p
printf("%d\n", *p); // 输出30
这时候,在内存中可以表示为:
地址 |
值 |
1000 |
20 |
1004 |
… |
… |
… |
1500 |
30 |
1504 |
… |
… |
… |
2000 |
1500 |
2008 |
… |
其中1500是b所在的地址,30是b的值;2000仍然是p所在的地址,但现在它的值改为了1500(也就是b 的地址);*p现在访问或修改了b所在内存单元中的值。
各种不同类型指针
一些常见的指针类型有:
-
基本类型指针:如 int *、char *、float * 等,用来指向基本类型的变量
-
数组指针:如 int ()[10]、char ()[20] 等,用来指向数组的首地址
-
指针数组:如 int *[5]、char *[10] 等,用来存储多个指针变量的数组
-
字符串指针:如 char *str 或 char str[],用来指向字符串常量或字符数组
-
结构体指针:如 struct student *stu,用来指向结构体变量或结构体数组
-
函数指针:如 void ()(int, char) 或 int ()(double),用来存储函数的地址,可以通过函数指针调用函数
代码示例:
#include <stdio.h>
#include <stdlib.h>// 整型指针
int *p1; // 声明一个整型指针变量 p1
int a = 10; // 声明一个整型变量 a 并赋值为 10
p1 = &a; // 把 a 的地址赋值给 p1,即 p1 指向 a
printf("The value of a is %d, the address of a is %p.\n", a, &a); // 打印 a 的值和地址
printf("The value of *p1 is %d, the address of p1 is %p.\n", *p1, p1); // 打印 *p1 的值和 p1 的地址// 字符型指针
char *p2; // 声明一个字符型指针变量 p2
char b = 'A'; // 声明一个字符型变量 b 并赋值为 'A'
p2 = &b; // 把 b 的地址赋值给 p2,即 p2 指向 b
printf("The value of b is %c, the address of b is %p.\n", b, &b); // 打印 b 的值和地址
printf("The value of *p2 is %c, the address of p2 is %p.\n", *p2, p2); // 打印 *p2 的值和 p2 的地址// 数组指针
int (*p3)[3]; // 声明一个数组指针变量 p3,它可以指向一个包含三个元素的整型数组
int c[3] = {1, 2, 3}; // 声明一个包含三个元素的整型数组 c 并初始化为 {1, 2, 3}
p3 = &c; // 把 c 的地址赋值给 p3,即 p3 指向 c
printf("The value of c[0] is %d, the address of c[0] is %p.\n", c[0], &c[0]); // 打印 c[0] 的值和地址
printf("The value of (*p3)[0] is %d, the address of (*p3)[0] is %p.\n", (*p3)[0], &(*p3)[0]); // 打印 (*p3)[0] 的值和地址// 函数指针
int add(int x, int y) { // 定义一个函数 add,它接受两个整型参数 x 和 y,并返回它们的和return x + y;
}
int (*p4)(int x,int y); // 声明一个函数指针变量 p4,它可以指向一个接受两个整型参数并返回整型结果的函数
// 注意:声明函数指针时要与所要调用的函数保持一致(参数类型、个数、顺序以及返回类型)
// 可以使用 typedef 简化函数指针的声明,例如:typedef int (*func)(int x,int y);
// 这样就可以用 func 来代替 int (*func)(int x,int y),简化了代码。// 赋值方式一:
// 直接将函数名赋给函数指针变量(因为函数名就是该函数在内存中存放位置的首地址)printf("\nThe first way to assign function pointer:\n");
printf("----------------------------------------\n");
printf("Address of function add: %#x\n",add);
printf("Address before assignment: %#x\n",*(&add));p4 = add; // 把 add 的地址赋值给 p4,即 p4 指向 add
printf("Address after assignment: %#x\n",p4);
printf("Result of calling function add(3, 5) through its name: %d\n",add(3, 5)); // 直接通过函数名调用 add 函数
printf("Result of calling function add(3, 5) through pointer: %d\n",p4(3, 5)); // 通过函数指针变量 p4 调用 add 函数// 赋值方式二:
// 使用 & 运算符取得函数的地址,并赋给函数指针变量printf("\nThe second way to assign function pointer:\n");
printf("----------------------------------------\n");
printf("Address of function add: %#x\n",&add);
printf("Address before assignment: %#x\n",*(&add));
p4 = &add; // 把 &add 的值赋值给 p4,即 p4 指向 add
printf("Address after assignment: %#x\n",p4);
printf("Result of calling function add(3, 5) through its name: %d\n",add(3, 5)); // 直接通过函数名调用 add 函数
printf("Result of calling function (*p4)(3, 5) through pointer: %d\n",(*p4)(3, 5)); // 通过函数指针变量 p4 调用 add 函数,注意要加括号 (*p4)
为什么需要指针?
-
指针可以让你直接操作内存地址,这样可以提高程序的效率和灵活性
-
指针可以实现函数的参数传递,这样可以修改函数外部的变量,或者传递大型的数据结构而不用复制
-
指针可以构建一些复杂的数据结构,如链表、树、图等,这些数据结构在很多问题中都有重要的应用
-
指针可以实现函数指针,这样可以把函数作为参数传递给其他函数,或者实现回调函数等功能
以上的代码实现必须用到指针,仅用变量是不能实现的,所以,一个合格的C开发者必须要掌握指针编程;一个必须用指针的实际例子是动态内存分配(在操作系统环境的编程中会经常用到),在程序运行过程中,根据需要申请和释放内存空间;这样可以节省内存资源,提高程序的灵活性和效率。
C语言中,动态内存分配的函数有 malloc、calloc、realloc 和 free。这些函数都需要使用指针来接收或传递申请或释放的内存地址;例如:
// 申请一个 int 类型的空间,返回该空间的地址给 p
int *p = (int *)malloc(sizeof(int));
// 判断是否申请成功
if (p == NULL) {printf("Memory allocation failed.\n");exit(1);
}
// 给该空间赋值为 10
*p = 10;
// 打印该空间的值和地址
printf("The value is %d, the address is %p.\n", *p, p);
// 释放该空间,传递该空间的地址给 free
free(p);
多级指针
C语言的多级指针是指指向指针的指针,也就是说,它存储的是另一个指针变量的地址;多级指针可以用来实现动态内存分配、链表、树等数据结构。
例如,下面这段代码定义了一个二级指针p,它可以用来修改一个一级指针q所指向的值:
int x = 10; // 定义一个整型变量x
int *q = &x; // 定义一个一级指针q,让它存储x的地址
int **p = &q; // 定义一个二级指针p,让它存储q的地址
**p = 20; // 通过二级指针p修改x的值
printf("%d\n", x); // 输出20
根据二级指针的定义方式,可以分为三种内存模型:
-
指针数组:char *arr[] = {“abc”, “def”, “ghi”}; 这种模型定义了一个指针数组,数组的每个元素都是一个地址,指向一个字符串常量
-
二维数组:char arr[3][5] = {“abc”, “def”, “ghi”}; 这种模型定义了一个二维数组,有3个(5个char)空间的存储变量
-
动态分配:char **arr = (char **)malloc(100 * sizeof(char *)); 这种模型定义了一个二级指针,开辟了100个指针空间,存放了100个地址
void*型指针
-
void *指针是一种特殊的指针类型,可以存放任意对象的地址
-
void *指针没有确定的类型,所以不能直接对其进行解引用或运算
-
void *指针可以通过强制类型转换来转换为其他类型的指针,从而实现多态性
-
void *指针可以作为函数的参数或返回值,表示不确定类型的数据
上栗子:
#include <stdio.h>
//定义一个函数,接受一个void *指针和一个int型变量,根据变量的值来打印不同类型的数据
void printData(void *p, int type)
{if (type == 0) //如果type为0,表示p是int型指针{printf("The int value is %d\n", *(int *)p); //强制转换为int型指针并解引用打印}else if (type == 1) //如果type为1,表示p是char型指针{printf("The char value is %c\n", *(char *)p); //强制转换为char型指针并解引用打印}
}int main()
{int a = 10; //定义一个int型变量achar b = 'A'; //定义一个char型变量bprintData(&a, 0); //调用函数,传入a的地址和0作为参数printData(&b, 1); //调用函数,传入b的地址和1作为参数return 0;
}
还有内容的,续更中…敬请期待