C语言基础知识总结

1、C关键字

C语言中的关键字有32个:

  • 数据类型关键字(12个)
    char, short, int, long, float, double, unsigned, signed, struct, union, enum, void
  • 控制语句关键字(12个)
    if, else, switch, case, default, for, do, while, break, continue, goto, return
  • 存储类关键字(5个)
    auto, extern, register, static, const
  • 其它关键字(3个)
    sizeof, typedef, volatile

 

2、数据类型

数据类型的作用:编译器预算对象(变量)分配的内存空间大小

 

3、常量

常量特点:

  • 在程序运行过程中,值不能被改变的量
  • 常量一般出现在表达式或者赋值语句中
整型常量 100,200,-1000,0
实型常量 3.14,0.125,-3.123
字符常量 'a','b','\r','\n'
字符串常量 "a","abc","2333"

 

4、变量

(1)变量

  • 在程序运行过程中,其值可以被改变
  • 变量在使用前必须先定义,定义变量前必须有相对应的数据类型

标识符命名规则

  • 标识符不能够是关键字
  • 标识符只能由字母、数字、下划线组成
  • 第一个字符必须是字母或者下划线
  • 标识符中的字母区分大小写

变量的特点:

  • 变量在编译的时候为其分配相应的内存空间大小
  • 可以通过变量的名字和变量的地址访问相应内存

(2)声明和定义区别

  • 声明变量不需要建立存储空间,如:extern int a;
  • 定义变量需要建立存储空间,如:int a;
  • int b;既是声明同时也是定义
  • 对于extern int b;只是声明不是定义

一般情况下,把建立存储空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”。

 

5、整型

(1)整型int类型变量定义和输出

打印格式 含义
%d 输出有符号的10进制int类型
%o 输出8进制的int类型
%x 输出16进制的int类型,字母以小写输出
%X 输出16进制的int类型,字母以大写输出
%u 输出10进制的无符号数
#include <stdio.h>

int main(void)
{
  int a = 123;  //定义变量a,以10进制方式赋值为123
  int b = 0456;  //定义变量b,以8进制方式赋值为456
  int c = 0xabc;  //定义变量c,以16进制方式赋值为0xabc

  printf("a = %d\r\n", a);  //10进制格式输出
  printf("b = %o\r\n", b);  //8进制格式输出
  printf("c = %x\r\n", c);  //16进制格式输出

  return 0;  
}

(2)整型变量的输入

#include <stdio.h>

int main(void)
{
  int a;

  printf("Please input the value of a:");
  scanf("%d", &a);

  printf("a = %d\r\n", a);

  return 0;
}

(3)整型short、int、long、long long

整型类型 占用空间
short(短整型) 2字节
int(整型) 4字节
long(长整型) Windows为4字节,Linux(32bit)为4字节,Linux(64bit)为8字节
long long(长整型) 8字节

注意:

  • 整型数据在内存中所占用的字节数与所选择的操作系统有关,C语言标准中没有明确规定整型数据的长度,但是long类型整数的长度不能短于int类型,short类型的长度不能长于int类型;
  • 当一个小的数据类型赋值给大的数据类型时,不会出错,编译器会自动转化,但当一个大的数据类型赋值给一个小的数据类型时,有可能会丢失高位。
整型常量 所需类型
10 代表int类型
10l,10L 代表long类型
10ll,10LL 代表long long类型
10u,10U 代表unsigned int类型
10ul,10UL 代表unsigned long类型
10ull,10ULL 代表unsigned long long类型

各整型类型的格式化输出:

打印格式 含义
%hd 输出short类型
%d 输出int类型
%ld 输出long类型
%lld 输出long long类型
%hu 输出unsigned short类型
%u 输出unsigned int类型
%lu 输出unsigned long类型
%llu 输出unsigned long long类型

整型格式化输出例子:

#include <stdio.h>

int main(void)
{
  short a = 10;
  int b = 10;
  long c = 10L;
  long long d = 10LL;

  printf("sizeof(a) = %u\r\n", sizeof(a));
  printf("sizeof(b) = %u\r\n", sizeof(b));
  printf("sizeof(c) = %u\r\n", sizeof(c));
  printf("sizeof(d) = %u\r\n", sizeof(d));

  unsigned short a2 = 20u;
  unsigned int b2 = 20u;
  unsigned long c2 = 20ul;
  unsigned long long d2 = 20ull;

  printf("unsigned short a = %hu\r\n", a2);
  printf("unsigned int b = %u\r\n", b2);
  printf("unsigned long = %lu\r\n", c2);
  printf("unsigned long long = %llu\r\n", d2);

  return 0;
}

(4)有符号数和无符号数的区别

  • 有符号数的最高位为符号位,0代表正数,1代表负数
#include <stdio.h>

int main(void)
{
  signed int a = -1089474374;  //定义有符号整型变量a
  printf("a = %X\r\n", a);  //结果16进制输出为a = BF0FF0BA

  //B    F    0    F     F     0    B    A
  //1011 1111 0000 1111  1111  0000 1011 1010

  return 0;
}
  • 无符号数最高位不是符号位,就是数的一部分,无符号数不可能是负数
#include <stdio.h>

int main(void)
{
  unsigned int a = 3236958022;  //定义无符号整型变量a
  printf("a = %X\r\n");  //16进制输出结果为C0F00F46

  //C    0    F    0     0     F     4     6
  //1100 0000 1111 0000  0000  1111  0100  0110

  return 0;
}

在编写程序处理一个不可能出现负值的数值时,一般使用无符号数,这样处理能增大数的表达最大值。

  • 有符号数和无符号数整型的取值范围
数值类型 占用空间 取值范围
short 2字节 -32768~32767(-215~215-1)
int 4字节  -2147483648~2147483647(-231~231-1) 
long 4字节  -2147483648~2147483647(-231~231-1) 
unsigned short 2字节 0~65535(0~216-1) 
unsigned int 4字节  0~4294967295(0~232-1)
unsigned long  4字节  0~4294967295(0~232-1) 

 

6、sizeof关键字

  • sizeof不是函数,是关键字,不需要包含任何头文件,它的功能是计算一个数据类型的大小,单位为字节
  • sizeof的返回值为size_t类型
  • size_t类型在32位操作系统下是unsigned int类型,为无符号整数
#include <stdio.h>

int main(void)
{
  int a;
  int b = sizeof(a);  //获取变量a所占用的内存大小,单位为字节

  printf("b = %d\r\n", b);

  size_t c = sizeof(a);
  printf("c = %u\r\n", c);  //使用无符号整数的格式输出
}

 

7、字符型

(1)字符型变量的定义和输出

字符型变量用于存储一个单一的字符,在C语言中,使用char数据类型进行表示,每个字符变量会占用1个字节,在给字符变量赋值时,需要使用一对英文半角格式的单引号(' ')把字符括起来。

字符变量实际上并不是把该字符本身存储到变量的内存单元中,而是将该字符对应的ASCII编码存储到变量的内存单元中,char类型的本质就是一个1字节大小的整型。

#include <stdio.h>

int main(int argc, char *argv[])
{
  char c = 'a';

  printf("sizeof(c) = %u\r\n", sizeof(c));

  printf("c = %c\r\n", c);
  printf("c = %d\r\n", c);

  return 0;
}

(2)字符型变量的输入

#include <stdio.h>

int main(int argc, char *argv[])
{
  char c;

  printf("Please input the char:");
  
  scanf("%c", &c);
  printf("c =  %c\r\n", c);

  return 0;
}

(3)ASCII码表

ASCII码大致由以下两部分组成:

  • ASCII非打印控制字符:ASCII表上的数字0~31分配给了控制字符,用于控制像打印机等一些外围设备;
  • ASCII打印字符:数字32~126分配给了能在键盘上找到的字符,当查看或者打印文档时就会出现,数字127代表Del命令。

(4)转义字符

转义字符 含义 ASCII码值(十进制)
\a 警报 007
\b 退格(BS),将当前位置移到前一列 008
\f 换页(FF),将当前位置移到下页开头 012
\n 换行(LF),将当前位置移到下一行开头 010
\r 回车(CR),将当前位置移到本行开头 013
\t 水平制表(HT),跳到下一个TAB位置 009
\v 垂直制表(VT) 011
\\ 代表一个反斜线'\'字符 092
\' 代表一个单引号'''字符 039
\" 代表一个双引号'"'字符 034
\? 代表一个问号字符 063
\0 数字0 000
\ddd 8进制转义字符,d范围0~7 3位8进制
\xhh 16进制转义字符,h范围0~9,a~f,A~F 3位16进制

简单示例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
  printf("abc");
  printf("\befg\n");  //\b为退格键,\n为换行键

  printf("%d\n", '\123');//'\123'为8进制转义字符,0123对应十进制数83
  printf("%d\n", '\x23');//'\x23'为16进制转义字符,0x23对应十进制数53

  return 0;
}

 

8、实型(浮点型)

实型变量也可以称为浮点型变量,浮点型变量是用来存储小数数值的,在C语言中,浮点型变量分为两种,分别为单精度浮点数(float)、双精度浮点数(double),但是,double类型变量表示的浮点数比float类型变量更精确。

由于浮点型变量是由有限的存储单元组成的,因此,只能提供有限的有效数字,在有效位以外的数字将会被舍弃,所以可能会产生一定的误差。

#include <stdio.h>

int main(int argc, char *argv[])
{
  //传统方法赋值
  float a = 3.14f;
  double b = 3.14;

  printf("a = %f\r\n", a);
  printf("b = %lf\r\n", b);

  //科学法赋值
  a = 3.2e3f;  //3.2*1000=3200
  printf("a = %f\r\n", a);

  a = 100e-3f;  //100*0.001=0.1
  printf("a = %f\r\n", a);

  return 0;
}

 

9、进制

进制就是进位制,是人们规定的一种进位方法,对于任何一种进制-X进制,就表示某一位置上的数运算时是逢X进一位,十进制就是逢十进一,十六进制就是逢十六进一,二进制是逢二进一,以此类推。

十进制 二进制 八进制 十六进制
0 0 0 0
1 1 1 1
2 10 2 2
3 11 3 3
4 100 4 4
5 101 5 5
6 110 6 6
7 111 7 7
8 1000 10 8
9 1001 11 9
10 1010 12 A
11 1011 13 B
12 1100 14 C
13 1101 15 D
14 1110 16 E
15 1111 17 F
16 10000 20 10

(1)二进制

二进制是计算机技术中广泛采用的一种数制,它是使用0和1两个数码来表示的数,基数为2,进位规则是逢二进一,借位规则是借一当二,目前计算机系统使用的基本是二进制系统,数据在计算机中主要是以补码的形式存储的。

术语 含义
bit(比特) 一个二进制代表一位,一个位只能表示0或1两种状态
Byte(字节) 一个字节有8bit,计算机中存储的最小单位是字节
WORD(双字节) 两个字节,16bit
DWORD 两个WORD,四个字节,32bit
1b 1bit
1B 1个字节,8bit
1k,1K 1024
1M 1024K
1G 1024M
1T 1024G
1Kb(千位) 1024bit
1KB(千字节) 1024字节
1Mb(兆位) 1024Kb=1024*1024bit
1MB(兆字节) 1024KB=1024*1024字节

十进制转化为二进制的方法如下:

用十进制数除以2,分别取余数和商数,当商数为0的时候,将余数倒着数就是转化后的结果。

十进制的小数转换成二进制的方法如下:

小数部分和2相乘,取整数,不足1取0,每次相乘都是小数部分,顺序看取整后的数就是转换后的结果。

(2)八进制

八进制,Octal,缩写为OCT或0,一种以8为基数的计数法,采用0~7八个数字,逢八进一,在编程语言中常以数字0开始表明该数字是八进制数,八进制的数和二进制数可以按位对应(八进制一位对应二进制三位),因此常用在计算机语言中。

十进制转化为八进制的方法:

用十进制数除以8,分别取余数和商数,当商数为0的时候,将余数倒着数就是转化后的结果。

(3)十六进制

十六进制,英文名称为Hexadecimal,它由0~9,A~F组成,字母不区分大小,与十进制的对应关系为,0~9对应0~9,A~F对应10~15。

十六进制数和二进制数可以按位相对应(十六进制一位对应二进制四位),因此常应用在计算机语言中。

十进制转化为十六进制数的方法:

用十进制数除以16,分别取余数和商数,当商数为0的时候,将余数倒着数就是转化后的结果。

(4)C语言表示进制数

在C语言中,表示各种进制数的方法如下:

进制 表示方法
十进制 以正常数字1~9开头,如123
八进制 以数字0开头,如0123
十六进制 以0x开头,如0x123
二进制 C语言不支持直接写二进制数

简单示例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
  int a = 123;  //十进制方式赋值
  int b = 0123;  //八进制方式赋值
  int c = 0x123;  //十六进制方式赋值

  printf("a = %d\r\n");
  printf("b = %o\r\n");
  printf("c = %x\r\n");

  return 0;
}

 

10、计算机内存数值存储方式

(1)原码

一个数的原码(原始的二进制码)具有以下特点:

  • 最高位作为符号位,0表示为正数,1表示为负数
  • 其它数值部分就是数值本身绝对值的二进制数
  • 负数的原码是在其绝对值的基础商,最高位变为1

原码表示简单示例如下:

十进制数 原码
+15 0000 1111
-15 1000 1111
+0 0000 0000
-0 1000 0000

原码表示方法简单易懂,与带符号数本身转换方便,只需要将符号还原即可,但当两个正数相减或不同符号数相加时,必须比较两个数哪个绝对值大,才能确定结果是正还是负,所以原码并不便于加减运算。

(2)反码

反码的含义:

  • 对于正数,反码与原码相同
  • 对于负数,符号位不变,其它部分取反(1变0,0变1)
十进制数 反码
+15 0000 1111
-15 1111 0000
+0 0000 0000
-0 1111 1111

反码运算也不方便,通常用来作为求补码的中间过渡。

(3)补码

在计算机系统中,数值一律使用补码来存储,补码的特点如下:

  • 对于正数,原码、反码、补码相同;
  • 对于负数,其补码为它的反码加1;
  • 补码符号位不变,其它位求反,最后整个数加1,得到原码。

计算机使用补码来存储的主要原因为:

  • 统一了零的编码;
  • 将符号位和其它位统一处理;
  • 将减法运算转变为加法运算;
  • 两个使用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。

(4)数值溢出

当超过一个数据类型能够存放的最大范围时,数值就会溢出。

有符号位最高位溢出的区别:符号位溢出会导致数的正负发生改变,但是最高位的溢出会导致最高位丢失。

数据类型 占用空间 取值范围
char 1字节 -128~127(-27~27-1)
unsigned char 1字节 0~255(0~28-1)
#include <stdio.h>

int main(int argc, char *argv[])
{
  char a;

  //符号位溢出会导致数的正负发生改变
  a = 0x7f + 2;  //127+2
  printf("a = %d\r\n", a);
  //0111 111
  //+2后为1000 0001,这是负数补码,其原码为1111 1111,结果是-127

  //最高位的溢出会导致最高位丢失
  unsigned char a2;
  a2 = 0xff + 1;  //255+1
  printf("a2 = %u\r\n", a2);
  //1111 1111
  //+1后为10000 0000,char类型只有8位,最高位溢出,结果为0

  a2 = 0xff + 2;  //255+2
  //1111 1111
  //+2后为10000 0001,char类型只有8位,最高位溢出,结果为1

  return 0;
}

 

11、字符串格式化输出和输入

(1)字符串常量

  • 字符串是内存中一段连续的char空间,以'\0'(数字0)结尾;
  • 字符串常量是由双引号括起来的字符序列,如"Hello World","$12.5"等都是合法的字符串常量。

字符串常量与字符常量的区别:

每个字符串常量的结尾,编译器会自动的添加一个结束标志位'\0',也就是"a"包含两个字符'a'和'\0','\0'是字符串结束符。

(2)printf函数和putchar函数

printf函数用来输出字符串,而putchar用来输出一个字符。

printf格式字符:

打印格式 对应数据类型 含义
%d int 有符号的十进制整数
%hd short int 短整数
%hu unsigned short 无符号短整数
%o unsigned int 无符号八进制整数
%u unsigned int 无符号十进制整数
%x,%X unsigned int 无符号十六进制整数
%f float 单精度浮点数
%lf double 双精度浮点数
%e,%E double 科学计数法表示的数
%c char 字符类型
%s char * 字符串类型
%p void * 以十六进制形输出指针
%% % 输出一个百分号

printf附加格式:

字符 含义
l字母(l) 附加在d,u,x,o前面,表示长整数
- 左对齐
m(代表一个整数) 数据最小宽度
0(数字0) 将输出的前面补0直到占满列宽为止
m.n(代表一个整数) m指定域宽,n指定精度,符点数小数位数

(3)scanf函数与getchar函数

  • getchar函数用来从标准输入设备读取一个char;
  • scanf通过%转义的方式可以得到用户通过标准输入设备输入的数据。

 

12、运算符和表达式

 

13、程序流程结构

 

14、数组和字符串

(1)概述

在程序设计中,为了方便处理数据,把具有相同类型的若干变量按有序形式组织起来的数据类型称为数组,数组就是在内存中连续的相同类型的变量空间,所有成员在内存中的地址是连续的。

数组属于构造数据类型:

  • 一个数组可以分解为多个数组元素,这些数组元素可以是基本的数据类型或者构造类型
int a[10];
struct student stu[10];
  • 按数组元素类型的不同,数组可以分为:数值数组、字符数组、指针数组、结构数组等
int a[10];
char str[10];
char *p[10];

通常情况下,数组元素下标的个数也称为维数,根据维数的不同,可将数组分为一维数组、二维数组、三维数组等,将二维及以上的数组称为多维数组。

(2)一维数组

(2.1)一维数组的定义和使用

  • 数组名符合标识符的书写规定(数字、英文字母、下划线)
  • 数组名不能与其它变量名相同,同一作用域内是唯一的
  • 方括号[]中常量表达式表示数组元素的个数
  • 定义数组时[]内最好是常量,使用数组时[]内可以是常量,也可以是变量

简单示例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
  int a[10];//定义数组a,有10个成员,每个成员都是int类型,a[0]~a[9]

  int i;
  for (i = 0; i < 10; i++)
    a[i] = i;  //数组元素赋值操作

  for (i = 0; i < 10; i++)
    printf("%d ", a[i]);
  printf("\r\n");

  return 0;
}

(2.2)一维数组的初始化

在定义数组的同时进行赋值,称为初始化,如果全局数组不初始化的话,编译器将其初始化为0,局部数组如果不初始化的话,变量将会是随机值。

int a[10] = {1,2,3,4,5,6,7,8,9,10};//定义数组同时初始化所有成员变量
int b[10] = {1,2,3};//定义数组同时初始化前3个成员,剩余成员变量为0
int c[10] = {0};//定义数组同时初始化所有成员变量为0
int d[] = {1,2,3,4,5};//[]中不定义元素个数,定义时必须初始化,5个成员变量

(2.3)数组名

数组名是一个地址的常量,代表数组中首元素的地址。

#include <stdio.h>

int main(int argc, char *argv[])
{
  int a[10] = {0};

  printf("a = %p\r\n", a);//数组首地址
  printf("&a[0] = %p\r\n", &a[0]);//数组首元素地址

  return 0;
}

(3)二维数组

(3.1)二维数组的定义和使用

二维数组定义的一般形式是:

类型说明符 数组名[常量表达式1][常量表达式2]

其中,常量表达式1表示第一维下标的长度,常量表达式2表示第二维下标的长度。

定义一个二维数组:

int a[3][4];
  • 命令规则同一维数组
  • 定义了一个三行四列的数组,数组名为a,其元素类型为int,数组的元素个数为3*4个,如下:

二维数组a是按行进行存放的,先存放a[0]行,再存放a[1]行、a[2]行,并且每行有4个元素,也是依次存放的。

  • 二维数组在概念上二维的,其下标在两个方向上变化,对其访问一般需要两个下标
  • 在内存中并不存在二维数组,二维数组实际的硬件存储是连续编址,内存中只有一维数组,存储第一行后,依次存储第二行,与一维数组存储方式是一样的

简单实例:

#include <stdio.h>

int main(int argc, char *argv[])
{
  int a[3][4];
  int val = 0;
  int i, j;

  for (i = 0; i < 3; i++)
  {
    for (j = 0; j < 4; j++)
      a[i][j] = val++;
  }
return 0; }

(3.2)二维数组的初始化

//分段赋值
int a[3][4] = {
    {1,2,3,4},
    {5,6,7,8},
    {9,10,11,12}
};

//连续赋值
int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

//部分元素赋初值,未初始化的成员变量为0
int a[3][4] = {1,2,3,4};

//[]中不定义元素个数,定义时必须初始化
int a[][4] = {1,2,3,4,5,6,7,8};

(3.3)数组名

数组名是一个地址的常量,代表数组中首元素的地址。

#include <stdio.h>

int main(int argc, char *argv[])
{
    //定义一个二维数组,数组名为a
    //本质上还是一维数组,该一维数组有3个元素
    //每个元素又是一个一维数组int[4]
    unsigned int a[3][4] = {
        {1,2,3,4},
        {5,6,7,8},
        {9,10,11,12}
    };

    printf("addr: a = %p\r\n", a); //数组名为首元素地址
    printf("addr: a[0] = %p\r\n", a[0]); //第0个一维数组首地址
    printf("addr: a[1] = %p\r\n", a[1]); //第1个一维数组首地址
    printf("addr: a[2] = %p\r\n", a[2]); //第2个一维数组首地址
    printf("addr: a[0][0] = %p\r\n", &a[0][0]); //二维数组首个元素地址

    //int类型为4个字节
    printf("size: sizeof(a) = %lu\r\n", sizeof(a)); //二维数组所占空间,结果48个字节(3 * 4 * 4)
    printf("size: sizeof(a[0]) = %lu\r\n", sizeof(a[0])); //第0个一维数组所占空间,结果为16个字节(4 * 4)
    printf("size: sizeof(a[0][0]) = %lu\r\n", sizeof(a[0][0])); //二维数组第一个元素所占空间,结果为4个字节

    //求二维数组行数
    printf("n = %lu\r\n", sizeof(a) / sizeof(a[0]));

    //求二维数组列数
    printf("m = %lu\r\n", sizeof(a[0]) / sizeof(a[0][0]));

    //求二维数组行列总数
    printf("n*m = %lu\r\n", sizeof(a) / sizeof(a[0][0]));

    return 0;
}

(4)字符数组与字符串

(4.1)字符数组与字符串的区别

  • C语言中没有字符串这种数据类型,可以使用char数组来替代
  • 字符串一定是一个char类型的数组,但char类型的数组未必是字符串
  • 数字0(和字符'\0'等价)结尾的char数组就是一个字符串,但如果char类型数组没有以数字0结尾,那么就不是一个字符串,只是普通数组,所以字符串是一种特殊的char类型数组。
#include <stdio.h>

int main(int argc, char *argv[])
{
  char str1[] = {'c', ' ', 'p', 'r', 'o', 'g'}; //定义字符数组并初始化
  printf("str1 = %s\r\n", str1);  //乱码,没有'\0'结束符

  //以'\0'结束符结尾的字符数组是字符串
  char str2[] = {'c', ' ', 'p', 'r', 'o', 'g', '\0'};
  printf("str2 = %s\r\n", str2);  //输出正常

  //字符串处理以'\0'作为结束符,后面的"hello"将不会输出
  char str3[] = {'c', ' ', 'p', 'r', 'o', 'g', '\0', 'h', 'e', 'l', 'l', 'o', '\0'};
  printf("str3 = %s\r\n", str3);

  return 0;
}

(4.2)字符串的初始化

#include <stdio.h>

//C语言没有字符串类型,通过字符数组模拟
//C语言字符串,以字符'\0'结束
int main(int argc, char *argv[])
{
  //不指定长度,没有'\0'结束符,长度就是元素个数
  char buf1[] = {'a', 'b', 'c'};
  printf("buf[1] = %s\r\n", buf1);  //乱码

  //指定长度,后面没有赋初值的元素,自动补0
  char buf2[10] = {'a', 'b', 'c'};
  printf("buf[2] = %s\r\n", buf2);

  //字符数组所有元素赋值为0
  char buf3[10] = {0};
  printf("buf[3] = %s\r\n", buf3);

  //数组越界
  //char buf4[2] = {'a', 'b', 'c'};

  char buf5[10] = {'a', 'b', 'c', '\0', 'd'};
  printf("buf[5] = %s\r\n", buf5);

  char buf6[10] = {'a', 'b', 'c', 0, 'd'};
  printf("buf[6] = %s\r\n", buf5);

  //使用字符串初始化,编译器自动在后面补0
  char buf7[10] = "abc";
  printf("buf[7] = %s\r\n", buf7);

  return 0;
}

 

15、函数

 

16、指针

(1)概述

(1.1)内存

内存的基本含义如下:

  • 存储器:计算机组成中,用来存储程序和数据,辅助CPU进行运算处理的重要部分
  • 内存:内部存储器,暂存程序和数据,掉电会丢失,例如:SRAM、SDRAM、DDR等
  • 外存:外部存储器,长时间保存程序和数据,掉电不会丢失,例如:FLASH、ROM、NAND、EMMC等

内存是沟通CPU和硬盘的桥梁:

  • 暂存CPU中的运算数据和执行程序
  • 暂存与硬盘等外部存储器交换的数据

(1.2)物理存储器和存储地址空间

物理存储器:实际存在的具体存储器芯片。

  • 主板上的内存条
  • 显卡上的RAM芯片
  • 各种适配卡上的RAM和ROM芯片

存储地址空间:对存储器编码的范围,软件上常说的内存是指这一层含义。

  • 编码:对每个物理存储单元(一个字节)分配一个号码
  • 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写

(1.3)内存地址

内存地址的含义:

  •  将内存抽象成一个很大的一维字符数组
  • 编码就是对内存的每一个字节分配一个32位或64位的编号(与处理器的位数有关)
  • 这个内存编号就是内存地址

内存中的每个数据都会分配相应的内存地址:

  • char类型:占一个字节就会分配一个地址
  • int类型:占四个字节就会分配四个地址
  • float类型、struct类型、数组、函数等也会分配相应的地址。

(1.4)指针和指针变量

 指针和指针变量的含义:

  • 内存区的每个字节都有一个编号,这就是地址
  • 如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并且确定它的内存地址(编号)
  • 指针的实质就是内存地址,指针就是地址,地址就是指针
  • 指针是内存单元的编号,指针变量就是存放地址的变量
  • 通常我们叙述时会把指针变量简称为指针,实际指针和指针变量的含义并不一样

(2)指针的基础知识

(2.1)指针变量定定义和使用

  •  指针也是一种数据类型,指针变量也是一种变量
  • 指针变量指向谁,就把谁的地址赋值给指针变量
  • *操作符操作的是指针变量所指向的内存空间

简单实例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    char b = 'A';

    printf("Addr: &a = %p, &b = %p\r\n", &a, &b);//打印变量a,b的地址

    //int *代表一种数据类型,int*指针类型,p是变量名
    //定义一个指针类型的变量,可以指向一个int类型变量
    int *p;
    p = &a; //将变量a的地址赋值给指针变量p,p也是一个变量,值是内存地址编号
    printf("Addr: p = %p\r\n", p);  //输出指针变量p的值
    printf("Value: *p = %d\r\n", *p);   //输出指针变量p所指向的值

    char *p2;
    p2 = &b; //将变量b的地址赋值给指针变量p2
    printf("Addr: p2 = %p\r\n", p2);
    printf("Value: *p2 = %c\r\n", *p2);

    return 0;
}

注意:&可以取得一个变量在内存中的地址,但是不能够获取寄存器变量,因为寄存器变量不在内存里,而是在CPU内部。

(2.2)通过指针间接修改变量的值

简单示例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    int b = 200;
    int *p;

    printf("Init_Value: a = %d, b = %d\r\n", a, b);

    p = &a;
    *p = 300;
    printf("a = %d, *p = %d\r\n", a, *p);

    p = &b;
    *p = 400;
    printf("b = %d, *p = %d\r\n", b, *p);

    return 0;
}

(2.3)指针大小

  • 使用sizeof()能够测量指针的大小
  • sizeof()测的是指针变量指向存储地址的大小
  • 在32位平台,所有的指针(地址)都是32位(4字节)
  • 在64位平台,所有的指针(地址)都是64位(4字节)

简单实例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int *p1 = NULL;
    int **p2 = NULL;
    char *p3 = NULL;
    char **p4 = NULL;

    printf("sizeof(p1) = %lu\r\n", sizeof(p1));
    printf("sizeof(p2) = %lu\r\n", sizeof(p2));
    printf("sizeof(p3) = %lu\r\n", sizeof(p3));
    printf("sizeof(p4) = %lu\r\n", sizeof(p4));

    return 0;
}

(2.4)野指针和空指针

指针变量也是变量,变量是可以任意赋值的,只要不越界即可,32位的为4字节,64位的为8字节,但是,任意数值赋值给指针变量没有意义,因为这样的指针就成为了野指针,该指针指向的区域是未知的,操作系统不允许操作该指针指向的内存区域,所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出现错误。

简单示例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int *p = NULL;
    int a = 100;

    p = a; //直接把a的值赋给p,指针变量p成为野指针
    p = 0x123456; //指针变量p成为野指针
    printf("[1]\r\n");

    *p = 1000;  //操作野指针指向的未知区域,内存出现问题
    printf("[2]\r\n");

    return 0;
}

野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量,在C语言中,可以把NULL赋值给此指针,这样就可以标志位空指针,NULL是一个值为0的宏常量。

#define NULL ((void *)0)

int *p = NULL;

(2.5)万能指针void *

void *指针可以指向任意变量的内存空间:

#include <stdio.h>

int main(int argc, char *argv[])
{
    void *p = NULL;
    int a = 100;
    
    printf("Value: a = %d\r\n", a);
    p = (void *)&a; //获取变量a的地址并强制转换void *指针,赋值给p

    //使用指针变量指向的内存时,转换为int *类型
    *((int *)p) = 1;
    printf("Value: a = %d\r\n", a);

    return 0;
}

(2.6)const修饰的指针变量

简单实例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    int b = 200;

    //指向常量的指针
    //修饰*,指针指向的内存区域不能修改,指针的指向可以改变
    const int *p1 = &a; //等价于int const *p1 = &a;
    //*p1 = 1000; //错误,指针指向的内存区域不能修改
    p1 = &b;    //正确,指针指向能够修改

    //指针常量
    //修饰p2,指针指向不能改变,指针指向的内存区域可以修改
    int *const p2 = &a;
    //p2 = &b;  //错误,指针指向不能改变
    *p2 = 2000; //正确,指针指向的内存区域可以修改

    return 0;
}

在编写程序的时候,指针作为函数参数时,如果不想修改指针指向内存区域的值,需要使用const修饰指针数据类型。

(3)指针和数组

(3.1)数组名

数组名字是数组的首元素地址,但它是一个地址常量:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};

    printf("a = %p\r\n", a);
    printf("&a[0] = %p\r\n", &a[0]);

    //a = 0x1234;   //错误,数组名是常量地址,不能修改

    return 0;
}

(3.2)指针操作数组元素

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int i;
    int n = sizeof(a) / sizeof(a[0]);

    printf("Addr: a = %p\r\n", a);
    for (i = 0; i < n; i++)
        printf("%d ", *(a + i));
    printf("\r\n");

    int *p = a; //定义指针变量p,用于保存数组a的首地址
    for (i = 0; i < n; i++)
        p[i] = p[i] + 1;
    
    printf("Addr: p = %p\r\n", p);
    for (i = 0; i < n; i++)
        printf("%d ", *(p + i));
    printf("\r\n");

    return 0;
}

(3.3)指针加减运算

(3.3.1)加法运算

  • 指针计算并不是简单的整数相加
  • 如果是一个int *类型指针,+1的结果是增加一个int的大小
  • 如果是一个char *类型指针,+1的结果是增加一个char的大小

简单实例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    int *p = &a;

    printf("Addr: p = %p\r\n", p);
    p += 2; //移动了两个int
    printf("Addr: p = %p\r\n", p);

    char b = 'A';
    char *p2 = &b;

    printf("Addr: p2 = %p\r\n", p2);
    p2 += 2; //移动了两个char
    printf("Addr: p2 = %p\r\n", p2);

    return 0;
}

通过改变指针指向操作数组的元素:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int i;
    int n = sizeof(a) / sizeof(a[0]);
    int *p = a;

    for (i = 0; i < n; i++)
    {
        printf("%d ", *p);
        p++;
    }
    printf("\r\n");

    return 0;
}

(3.3.2)减法运算

简单实例1:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int i;
    int n = sizeof(a) / sizeof(a[0]);
    int *p = a + n - 1;

    for (i = 0; i < n; i++)
    {
        printf("%d ", *p);
        p--;
    }
    printf("\r\n");

    return 0;
}

简单实例2:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int *p1 = &a[1]; //第1个元素地址
    int *p2 = &a[2]; //第2个元素地址

    printf("Addr: p1 = %p, p2 = %p\r\n", p1, p2);

    int n1 = p2 - p1;   //n1 = 1
    int n2 = (unsigned long long)p2 - (unsigned long long)p1; //n2 = 4
    printf("n1 = %d, n2 = %d\r\n", n1, n2);

    return 0;
}

(3.4)指针数组

指针数组,它是数组,数组的每个元素都是指针类型的。

简单实例如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int *p[3];
    int a = 1, b = 2, c = 3;
    int i;

    p[0] = &a;
    p[1] = &b;
    p[2] = &c;

    for (i = 0; i < 3; i++)
    {
        printf("Addr: p[%d] = %p,", i, p[i]);
        printf("Value: *p[%d] = %d\r\n", i, *p[i]);
    }

    return 0;
}

(4)多级指针

  • C语言中允许有多级指针的存在,在实际的程序中一级指针最常用,其次是二级指针
  • 二级指针就是指向一个一级指针变量地址的指针
  • 三级指针依次类推

简单实例:

#include <stdio.h>

int main(int argc, char *argv[])
{
    int a = 100;
    int *p1;    //一级指针
    int **p2;   //二级指针
    int ***p3;  //三级指针

    //*p1就是变量a的值
    p1 = &a;
    printf("Addr: p1 = %p, Value = %d\r\n", p1, *p1);

    //*p2就是指针变量p1的值
    //**p2就是变量a的值
    p2 = &p1;
    printf("Addr: p2 = %p, *p2 = %p, Value = %d\r\n", p2, *p2, **p2);

    //*p3就是指针变量p2的值
    //**p3就是指针变量p1的值
    //***p3就是指针变量a的值
    p3 = &p2;
    printf("Addr: p3 = %p, *p3 = %p, **p3 = %p, Value = %d\r\n", p3, *p3, **p3, ***p3);

    return 0;
}

(5)指针和函数

(5.1)函数形参改变实参的值

简单实例如下:

#include <stdio.h>

void swap1(int x, int y)
{
    int temp;

    temp = x;
    x = y;
    y = temp;

    printf("Addr: &x = %p, &y = %p\r\n", &x, &y);
    printf("Value: x = %d, y = %d\r\n", x, y);
}

void swap2(int *x, int *y)
{
    int temp;

    temp = *x;
    *x = *y;
    *y = temp;

    printf("Addr: x = %p, y = %p\r\n", x, y);
    printf("Value: *x = %d, *y = %d\r\n", *x, *y);
}

int main(int argc, char *argv[])
{
    int a = 1;
    int b = 2;

    printf("Addr: &a= %p, &b = %p\r\n", &a, &b);

    swap1(a, b); //值传递
    printf("[1]: a = %d, b = %d\r\n", a, b);

    a = 3;
    b = 4;
    swap2(&a, &b); //地址传递
    printf("[2]: a = %d, b = %d\r\n", a, b);

    return 0;
}

(5.2)数组名做函数参数

使用数组名做函数参数,函数的形参会退化为指针:

#include <stdio.h>

void print_arry(int *arry, int n)
{
    int i;

    for (i = 0; i < n; i++)
        printf("%d ", *(arry + i));
    printf("\r\n");
}

int main(int argc, char *argv[])
{
    int a[] = {1, 2, 3, 4, 5};
    int n = sizeof(a) / sizeof(a[0]);

    //数组名作为函数参数
    print_arry(a, n);

    return 0;
}

(5.3)指针作为函数的返回值

#include <stdio.h>

static int g_a = 100;

int *get_global_a_addr(void)
{
    return &g_a;
}

int main(int argc, char *argv[])
{
    int *p = NULL;
    
    p = get_global_a_addr();
    printf("Addr: p = %p\r\n", p);
    printf("Value: *p = %d\r\n", *p);

    *p = 1000;
    printf("Value: *p = %d, g_a = %d\r\n", *p, g_a);

    return 0;
}

(6)指针和字符串

(6.1)字符指针

#include <stdio.h>


int main(int argc, char *argv[])
{
    char string[] = "hello world";
    char *p = string;
    char *p2;

    printf("[1]: string = %s, p = %s\r\n", string, p);
    *p = 'H';
    p = p + 6;
    *p = 'W';
    p = string;
    printf("[2]: string = %s, p = %s\r\n", string, p);

    p2 = "Test String";
    printf("p2 = %s\r\n", p2);

    return 0;
}

(6.2)字符指针作为函数参数

#include <stdio.h>

void mystrcat(char *dest, const char *src)
{
    unsigned int len1 = 0, len2 = 0, i;

    while (dest[len1])
        len1++;
    
    while (src[len2])
        len2++;
    
    for (i = 0; i < len2; i++)
        dest[len1 + i] = src[i];
}

int main(int argc, char *argv[])
{
    char string1[100] = "Hello World";
    char string2[] = ",Test String.";

    mystrcat(string1, string2);
    printf("string1 = %s\r\n", string1);

    return 0;
}

(6.3)const修饰的指针变量

#include <stdio.h>

int main(int argc, char *argv[])
{
    //const修饰一个变量时为只读
    const int a = 100;
    //a = 100   //错误,变量不能被修改

    //指针变量,指针指向的内存,两个不同概念
    char string[] = "hello world";
    char buf[] = "Hello World";

    //从左往右看,跳过数据类型,看修饰哪个字符
    //如果是*,说明指针指向的内存不能被改变
    //如果是指针变量,说明指针的指向不能改变
    const char *p = string; //等价于char const *p = string;
    //p[0] = 'H';   //错误,指针指向的内存不能被改变
    printf("[1]: Addr->p = %p, Value->p = %s\r\n", p, p);
    
    p = buf;    //正确,指针的指向能够改变
    printf("[2]: Addr->p = %p, Value->p = %s\r\n", p, p);

    char *const p2 = string; //指针的指向不能改变,指针指向的内存能改变
    p2[0] = 'H'; //正确
    p2[6] = 'W'; //正确
    //p2 = buf  //错误,指针的指向不能改变
    printf("[3] = Addr->p2 = %p, Value->p2 = %s\r\n", p2, p2);

    //p3为只读,指针的指向和指针指向的内存都不能改变
    const char * const p3 = buf;
    //p3[0] = 'h'; //错误
    //p3 = string; //错误

    return 0;
}

(6.4)指针数组作为main函数的形参

int main(int argc, char *argv[])
  • main函数是由操作系统调用的,第一个参数标明argv数组的成员数量,第二个参数为指针数组,argv数组的每个成员都是char *类型
  • argv是命令行参数的字符串数组
  • argc是命令行参数的数量,程序名本身算一个参数

指针数组的简单示例:

#include <stdio.h>

//argc为传参数的个数(包含可执行程序)
//argv为指针数组,指向输入的字符串参数
int main(int argc, char *argv[])
{
    //指针数组,它是数组,并且每个元素都是指针变量
    char *str[] = {"123", "456", "789"};
    int n = sizeof(str) / sizeof(str[0]);
    int i;

    for (i = 0; i < n; i++)
        printf("Addr: str[%d] = %p, Value: str[%d] = %s\r\n", i, str[i], i, str[i]);

    for (i = 0; i < argc; i++)
        printf("Addr: argv[%d] = %p, Value: argv[%d] = %s\r\n", i, argv[i], i, argv[i]);

    return 0;
}

(6.5)常用字符串应用模型

(6.5.1)strstr中的while和do-while模型

利用字符串操作的标准库函数strstr找出一个字符串中子串出现的个数。

(a)while模型

#include <stdio.h>
#include <string.h>

unsigned int find_substr(const char *str, const char *substr)
{
    const char *temp = str;
    unsigned int n = 0;

    while ((temp = strstr(temp, substr)) != NULL)
    {
        n++;
        temp = temp + strlen(substr); //重新设置起点位置
        if (*temp == '\0') //判断是否到结束位置
            break;
    }

    return n;
}

int main(int argc, char *argv[])
{
    char *str = "123abc456abc789abcqweabcrrr";
    char *substr = "abc";
    unsigned int n = 0;

    n = find_substr(str, substr);
    printf("n = %d\r\n", n);

    return 0;
}

(b)do-while模型

#include <stdio.h>
#include <string.h>

unsigned int find_substr(const char *str, const char *substr)
{
    const char *temp = str;
    unsigned int n = 0;

    do
    {
        temp = strstr(temp, substr);
        if (temp != NULL)
        {
            n++;
            temp = temp + strlen(substr); //重新设置查找起点
        }
        else
            break;
        
    } while (*temp != '\0');

    return n;
}

int main(int argc, char *argv[])
{
    char *str = "123abc456abc789abcqweabcrrr";
    char *substr = "abc";
    unsigned int n = 0;

    n = find_substr(str, substr);
    printf("n = %d\r\n", n);

    return 0;
}

(6.5.2)两头堵模型

求非空字符串元素的个数。

#include <stdio.h>
#include <string.h>

int test_fun(const char *str, int *n)
{
    if (!str || !n)
        return -1;
    
    int begin = 0;
    int end = strlen(str) - 1;

    //从左边开始
    //如果当前字符为空,而且没有结束
    while (str[begin] == ' ' && str[begin] != '\0')
        begin++;    //位置向右移动一位
    
    //从右往左移动
    while (str[end] == ' ' && end > 0)
        end--;
    if (end == 0)
        return -1;
    
    //非空元素个数
    *n = end - begin + 1;

    return 0;
}

int main(int argc, char *argv[])
{
    const char *str = "    123456     ";
    int n = 0;

    test_fun(str, &n);
    printf("n = %d\r\n", n);

    return 0;
}

(6.5.3)字符串反转模型(逆置)

简单实例如下:

#include <stdio.h>
#include <string.h>

int str_inverse(char *string, int len)
{
    if (!string)
        return -1;
    
    char *p1 = string;
    char *p2 = string + len - 1;
    char temp;
    int n, i;

    if ((len % 2) == 0)
        n = len / 2;
    else
        n = (len - 1) / 2;
    
    for (i = 0; i < n; i++)
    {
        temp = *p1;
        *p1 = *p2;
        *p2 = temp;
        p1++;
        p2--;
    }

    return 0;
}

int main(int argc, char *argv[])
{
    char string[] = "123456789";
    int len = strlen(string);

    printf("[1]: string = %s\r\n", string);
    str_inverse(string, len);
    printf("[2]: string = %s\r\n", string);

    return 0;
}

(6.6)常见字符串处理函数

(6.6.1)strcpy函数

#include <string.h>

char *strcpy(char *dest, const char *src);
功能:
    将src所指向的字符串复制到dest所指向的空间中,'\0'也会拷贝过去
参数:
    dest: 目的字符串首地址
    src: 源字符串首地址
返回值:
    成功:返回dest字符串首地址
    失败:NULL
注意:
    如果参数dest所指向的内存空间不够大,可能造成缓冲溢出错误情况

简单实例代码如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = {0};
    char str2[20] = "Hello World";
    char *temp = NULL;

    printf("[1]: str1 = %s, str2 = %s\r\n", str1, str2);
    temp = strcpy(str1, str2);
    if (!temp)
        return -1;

    printf("[2]: str1 = %s, temp = %s\r\n", str1, temp);

    return 0;
}

(6.6.2)strncpy函数

#include <string.h>

char *strncpy(char *dest, const char *src, size_t n);
功能:
    将src所指向的字符串的前n个字符复制到dest所指向的空间中,
    是否拷贝'\0'取决于拷贝的字符长度是否包含
参数:
    dest: 目的字符串首地址
    src: 源字符串首地址
    n: 指定需要拷贝字符串个数
返回值:
    成功:返回dest字符串首地址
    失败:NULL

简单实例代码如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = {0};
    char str2[20] = "Hello World";
    char *temp = NULL;

    printf("[1]: str1 = %s, str2 = %s\r\n", str1, str2);
    temp = strncpy(str1, str2, sizeof(str1));
    if (!temp)
        return -1;

    printf("[2]: str1 = %s, temp = %s\r\n", str1, temp);

    return 0;
}

(6.6.3)strcat函数

#include <string.h>

char *strcat(char *dest, const char *src);
功能:
    将src所指向的字符串连接到dest字符串的尾部,'\0'也会追加
参数:
    dest: 目的字符串首地址
    src: 源字符串首地址
返回值:
    成功:返回dest字符串的首地址
    失败:NULL

简单示例如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = "123456";
    char str2[20] = "Hello World";
    char *temp = NULL;

    printf("[1]: str1 = %s, str2 = %s\r\n", str1, str2);
    temp = strcat(str1, str2);
    if (!temp)
        return -1;

    printf("[2]: str1 = %s, temp = %s\r\n", str1, temp);

    return 0;
}

(6.6.4)strncat函数

#include <string.h>

char *strncat(char *dest, const char *src, size_t n);
功能:
    将src所指向的字符串的前n个字符连接到dest字符串的尾部,
    '\0'是否追加取决于长度中是否包含
参数:
    dest: 目的字符串首地址
    src: 源字符串首地址
    n: 指定需要追加的字符个数
返回值:
    成功:返回dest字符串的首地址
    失败:NULL

简单实例如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = "123456";
    char str2[20] = "Hello World";
    char *temp = NULL;

    printf("[1]: str1 = %s, str2 = %s\r\n", str1, str2);
    temp = strncat(str1, str2, 5);
    if (!temp)
        return -1;

    printf("[2]: str1 = %s, temp = %s\r\n", str1, temp);

    return 0;
}

(6.6.5)strcmp函数

#include <string.h>

int strcmp(const char *s1, const char *s2);
功能:
    比较s1字符串和s2字符串的大小,比较的是字符ASCII码的大小
参数:
    s1: 字符串1首地址
    s2:字符串2首地址
返回值:
    相等:0
    大于:> 0
    小于:< 0

简答示例代码如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = "Hello World";
    char str2[25] = "Hello world";
    int n = 0;

    n = strcmp(str1, str2);
    if (n == 0)
        printf("str1 == str2\r\n");
    else if (n > 0)
        printf("str1 > str2\r\n");
    else
        printf("str1 < str2\r\n");

    return 0;
}

(6.6.6)strncmp函数

#include <string.h>

int strncmp(const char *s1, const char *s2, size_t n);
功能:
    比较s1字符串和s2字符串的前n个字符大小,比较的是字符ASCII码的大小
参数:
    s1: 字符串1首地址
    s2:字符串2首地址
    n: 指定比较的字符个数
返回值:
    相等:0
    大于:> 0
    小于:< 0

简单示例代码如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str1[25] = "Hello World";
    char str2[25] = "Hello world";
    int n = 0;

    n = strncmp(str1, str2, 6);
    if (n == 0)
        printf("str1 == str2\r\n");
    else if (n > 0)
        printf("str1 > str2\r\n");
    else
        printf("str1 < str2\r\n");

    return 0;
}

(6.6.7)sprintf函数

#include <stdio.h>

int sprintf(char *str, const char *format, ...);
功能:
    根据参数format字符串来转换并格式化数据,然后将结果输出到str指定
    的空间中,直到出现字符串结束符'\0'为止
参数:
    str: 结果输出缓冲区首地址
    format: 字符串格式,用法和printf一样
返回值:
    成功:实际格式化的字符个数
    失败:-1

简单示例代码如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    char str[100] = {0};
    int a = 100;
    char buf[] = "Hello World";
    int ret = 0;

    printf("a = %d, buf = %s\r\n", a, buf);
    ret = sprintf(str, "a:%d,buf:%s", a, buf);
    printf("ret = %d, str = %s\r\n", ret, str);

    return 0;
}

(6.6.8)sscanf函数

#include <stdio.h>

int sscanf(const char *str, const char *format, ...);
功能:
    从str指定的字符串读取数据,并根据参数format字符来转换并格式化
    数据
参数:
    str: 指定的字符串首地址
    format: 字符串格式,用户和scanf一样
返回值:
    成功:参数数目,成功转换的值的个数
    失败:-1

简单示例代码如下:

#include <stdio.h>

int main(int argc, char *argv[])
{
    char str[] = "a = 10, b = 20";
    int a, b;
    int ret = 0;

    ret = sscanf(str, "a = %d, b = %d", &a, &b);
    printf("ret = %d, a = %d, b = %d\r\n", ret, a, b);

    return 0;
}

(6.6.9)strchr函数

#include <string.h>

char *strchr(const char *str, int c);
功能:
    在字符串str中查找字符c出现的位置
参数:
    str: 要查找的字符串首地址
    c: 要查找的字符
返回值:
    成功:返回第一次出现字符c的地址
    失败:NULL

简单示例代码如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str[] = "Hello World";
    char *p;

    p = strchr(str, 'W');
    printf("str = %s, p = %s\r\n", str, p);

    return 0;
}

(6.6.10)strstr函数

#include <string.h>

char *strstr(const char *haystack, const char *needle);
功能:
    在字符串haystack中查找字符串needle出现的位置
参数:
    haystack: 源字符串首地址
    needle: 匹配字符串首地址
返回值:
    成功:返回第一次出现needle地址
    失败:NULL

简单示例代码如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str[] = "123abc456abc789";
    char substr[] = "abc";
    char *p;

    p = strstr(str, substr);
    printf("str = %s, p = %s\r\n", str, p);

    return 0;
}

(6.6.11)strtok函数

#include <string.h>

char *strtok(char *str, const char *delim);
功能:
    将字符串分割成一个个片段,当strtok()在参数str中发现参数delim中
    包含的分割字符时,则会将该字符改成'\0'字符,当连续出现多个时只
    替换第一个为'\0'
参数:
    str: 指向要分割的字符串
    delim: 分割字符串中包含的所有字符
返回值:
    成功:分割后字符串首地址
    失败:NULL
注意:
    在第一次调用时,strtok()必须给予参数str字符串
    在往后的调用则将参数str设置成NULL,每次调用成功返回指向被分割片段
    的指针

简单示例代码如下:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
    char str[] = "123s*abcgggd*5454ssg$yui";
    char *p = strtok(str, "*");

    while (p != NULL)
    {
        printf("p = %s\r\n", p);
        p = strtok(NULL, "*");
    }
}

(6.6.12)atoi函数

#include <stdlib.h>

int atoi(const char *nptr);
功能:
    字符串转int类型,扫描nptr字符串,跳过前面的空格字符,直到遇到数字或正负号才开始
    转换,而遇到非数字或字符串结束符'\0'才结束转换,并将结果返回
参数:
    nptr: 要转换的字符串
返回值:
    成功转换后返回整数

类似的函数有:
    atof(): 将一个小数形式的字符串转换为一个浮点数
    atol(): 将一个字符串转换为long类型

简单示例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char str[] = "  -10";
    int num = 0;

    num = atoi(str);
    printf("num = %d\r\n", num);

    return 0;
}

(6.7)指针小结

定义 说明
int i; 定义整型变量
int *p; 定义一个指向int类型的指针变量
int a[10]; 定义一个有10个元素的数组,每个元素类型为int
int *p[10]; 定义一个有10个元素的数组,每个元素类型为int *
int func(); 定义一个函数,函数返回值为int类型
int *func(); 定义一个函数,函数返回值为int *类型
int **p; 定义一个指向int的指针的指针,二级指针

 

17、内存管理

(1)作用域

C语言变量的作用域分为:

  • 代码块作用域(代码块是{}之间的一段代码)
  • 函数作用域
  • 文件作用域

(1.1)局部变量

局部变量也叫auto自动变量(auto可写可不写),一般情况下代码块{}内部定义的变量都是自动变量,它有如下特点:

  • 在一个函数内定义,只在函数范围内有效
  • 在复合语句中定义,只在复合语句中有效
  • 随着函数调用的结束或复合语句的结束,局部变量的声明周期也结束
  • 如果没有赋初值,变量的值是随机的

简单代码示例:

#include <stdio.h>

void test(void)
{
  //auto写不写是一样的
  //auto只能出现在{}内部
  auto int a = 10; //定义局部变量a并赋初值10
}

int main(int argc, char *argv[])
{
  //a = 100; //错误,main作用域内没有变量a

  if (1)
  {
    //在复合语句中定义,变量在复合语句中有效
    int a = 100;
    printf("a = %d\r\n", a);
  }

  //a = 10; //错误,离开复合语句后,变量a已经不存在

  return 0;
}

(1.2)静态(static)局部变量

  • static局部变量的作用域也是在定义的函数内有效
  • static局部变量的生命周期和程序运行周期一样,同时static局部变量的值只初始化一次,但可以赋值多次
  • static局部变量若未赋初值,则由系统自动赋值,数值型变量自动赋值为0,字符型变量赋空字符
#include <stdio.h>

void fun1(void)
{
  int a = 0;
  
  a++;
  printf("fun1: a = %d\r\n", a);
}

void fun2(void)
{
  //静态局部变量,没有赋初值,系统初始化为0,只初始化一次
  static int a;

  a++;
  printf("fun2: a = %d\r\n", a);
}

int main(int argc, char *argv[])
{
  fun1();
  fun1();
  fun2();
  fun2();

  return 0;
}

(1.3)全局变量

  • 在函数外定义,可被文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,需要使用extern声明
  • 全局变量的生命周期和程序运行周期一样
  • 不同文件的全局变量不可重名

(1.4)静态(static)全局变量

  • 在函数外定义,作用范围被限制在所定义的文件中
  • 不同文件静态全局变量可以重名,但作用域不冲突
  • static全局变量的生命周期和程序运行周期一样,同时static全局变量的值只初始化一次

(1.5)extern全局变量声明

extern int a;声明一个变量,这个全局变量在别的文件中已经定义,这里只是声明,不是定义。

(1.6)全局函数和静态函数

在C语言中函数默认是全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义在这个函数的文件中使用,在其它文件中不能被调用,即使在其他文件中声明这个函数都没用。

对于不同文件中的static函数名字可以相同。

注意:

  • 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的内存单元,互不干扰
  • 同一个源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用
  • 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是static函数,那么作用域是文件内的,所有不同的文件static函数名是可以相同的

(1.7)小结

类型 作用域 生命周期
auto变量 一对{}内 当前函数
static局部变量 一对{}内 整个程序运行期
extern变量 整个程序 整个程序运行期
static全局变量 当前文件 整个程序运行期
extern函数 整个程序 整个程序运行期
static函数 当前文件 整个程序运行期
register变量 一对{}内 当前程序
全局变量 整个程序 整个程序运行期

(2)内存布局

(2.1)内存分区

C源代码经过预处理、编译、汇编、链接4步后将生成一个可执行程序,在Windows系统下,程序是一个普通的可执行文件,以下简单列出一个二进制可执行文件的基本情况:

在上面可以得知,在没有运行程序前,也就是程序没有加载到内存前,可执行程序内部已经分好3段信息,分别为代码区(text)、数据区(data)、和未初始化数据区(bss)这3个部分,有的人直接把data和bss合起来叫做静态区和全局区。

  • 代码区(text段)

用来存放CPU执行的机器指令,通常代码区是可以共享的,也就是另外的执行程序可以调用它,使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可,代码区通常是只读的,原因是防止程序意外地修改了它的指令,另外代码区还规划了局部变量的相关信息。

  • 全局初始化数据区/静态数据区(data段)

该区包含了在程序中明确被初始化的全局变量,已经初始化的静态变量,包括静态全局变量和静态局部变量,还有常量数据,例如字符串常量。

  • 未初始化数据区(bss段)

该区存入的是全局未初始化变量和未初始化的静态变量,未初始化数据区的数据在程序开始执行之前会被内核初始化为0或者NULL。

程序在加载到内存前,代码区和全局区(data段和bss段)的大小就是固定的,程序在运行期间不能改变,当运行可执行程序后,系统把程序加载到内存,除前面分析到的text段、data段、bss段之外,额外增加栈区、堆区。

  • 代码区(text段)

加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存在程序运行期间不能够被修改。

  • 未初始化数据区(bss段)

加载的是可执行文件bss段,位置可以分开也可以紧靠data段,存储全局未初始化和静态未初始化数据,数据的生存周期为整个程序运行过程。

  • 全局初始化数据区/静态数据区(data段)

加载的是可执行文件的data段,存储全局初始化和静态初始化数据,字符常量,数据的生存周期为整个程序运行过程。

  • 栈区(stack)

栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等,在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。

  • 堆区(heap)

堆是一个大容器,它的容量要远远大于栈,但是没有栈那样先进后厨顺序,用于动态内存分配,堆区在内存中位于bss段和栈区之间,一般是由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

(2.2)存储类型小结

类型 作用域 生命周期 存储位置
auto变量 一对{}内 当前函数 栈区
static局部变量 一对{}内 整个程序运行期 初始化在data段,未初始化在bss段
extern变量 整个程序 整个程序运行期 初始化在data段,未初始化在bss段
static全局变量 当前文件 整个程序运行期 初始化在data段,未初始化在bss段
extern函数 整个程序 整个程序运行期 代码区
static函数 当前文件 整个程序运行期 代码区
register变量 一对{}内 当前函数 运行时存储在CPU寄存器
字符串常量 当前文件 整个程序运行期 data段

简单实例代码如下:

#include <stdio.h>
#include <stdlib.h>

int g_a;
static int g_b;
int g_c = 100;
static int g_d = 1000;

int main(int argc, char *argv[])
{
  int a;
  int b = 100;
  static int c;
  static int d = 100;
  char *i = "Hello World";
  char *k = (char *)malloc(100);

  printf("&a = %p\r\n", &a);  //局部未初始化变量(栈区)
  printf("&b = %p\r\n", &b);  //局部初始化变量(栈区)

  printf("&c = %p\r\n", &c);  //静态局部未初始化变量(bss段)
  printf("&d = %p\r\n", &d);  //静态局部初始化变量(data段)

  printf("&g_a = %p\r\n", &g_a);  //全局未初始化变量(bss段)
  printf("&g_b = %p\r\n", &g_b);  //静态全局未初始化变量(bss段)

  printf("&g_c = %p\r\n", &g_c);  //全局初始化变量(data段)
  printf("&g_d = %p\r\n", &g_d);  //静态全局初始化变量(data段)

  printf("i = %p\r\n", i);    //只读数据(常量区)
  printf("k = %p\r\n", k);    //动态分配内存(堆区)

  free(k);
  return 0;
}

(2.3)内存操作函数

(2.3.1)memset函数

#include <string.h>

void *memset(void *s, int c, size_t n);
功能:
    将内存区域s的前n个字节以参数c进行填入
参数:
    s: 要操作的内存区域首地址
    c: 填充的字符,c的参数int类型,但必须是unsigned char类型的范围0~255
    n: 指定要填充的字节个数
返回值:
    内存区域s的首地址

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char buf[10];

    memset(buf, 'A', sizeof(buf));

    for (int i = 0; i < sizeof(buf); i++)
        printf("buf[%d] = %c\r\n", i, buf[i]);

    return 0;
}

(2.3.2)memcpy函数

#include <string.h>

void *memcpy(void *dest, const void *src, size_t n);
功能:
    拷贝内存区域src的前n个字节到内存区域dest中
参数:
    dest: 目的内存首地址
    src: 源内存首地址
    n: 需要拷贝的字节数
返回值:
    内存区域dest首地址

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char buf[50] = "Hello World";
    char str[50] = {0};

    printf("[1]: str = %s\r\n", str);
    memcpy(str, buf, strlen(buf));
    printf("[2]: str = %s\r\n", str);

    return 0;
}

(2.3.3)memmove函数

#include <string.h>

void *memmove(void *dest, const void *src, size_t n);
功能:
    拷贝内存区域src的前n个字节到内存区域dest中
参数:
    dest: 目的内存首地址
    src: 源内存首地址
    n: 需要拷贝的字节数
返回值:
    内存区域dest首地址

注意:memmove()函数的用法和memcpy()函数的用法一样,区别在于,dest和src所指向的内存空间重叠时,memmove()函数仍然能处理,不过执行效率比memcpy()函数低些。

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char buf[50] = "Hello World";
    char str[50] = {0};

    printf("[1]: str = %s\r\n", str);
    memmove(str, buf, strlen(buf));
    printf("[2]: str = %s\r\n", str);

    return 0;
}

(2.3.4)memcmp函数

#include <string.h>

int memcmp(const void *s1, const void *s2, size_t n);
功能:
    比较s1和s2所指向内存区域的前n个字节
参数:
    s1: 内存首地址s1
    s2: 内存首地址s2
    n: 需要比较的前n个字节
返回值:
    相等:= 0
    大于:> 0
    小于:< 0

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char str1[50] = "Hello World";
    char str2[50] = "Hello world";
    int ret;

    ret = memcmp(str1, str2, 25);
    if (ret == 0)
        printf("str1 == str2\r\n");
    else if (ret > 0)
        printf("str1 > str2\r\n");
    else
        printf("str1 < str2\r\n");

    return 0;
}

(2.4)堆区内存分配和释放

(2.4.1)malloc函数

#include <stdlib.h>

void *malloc(size_t size);
功能:
    在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放
    类型说明符指定的类型,分配的内存空间内容不确定,一般使用memset函数初始化
参数:
    size: 需要分配的内存大小(字节单位)
返回值:
    成功:分配的内存空间起始地址
    失败:NULL

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    char *buf = NULL;

    buf = malloc(50);   //在堆区申请50个字节内存区域
    if (!buf)
    {
        printf("malloc buf failed\r\n");
        return -1;
    }

    memset(buf, 0, 50);
    printf("[1]: buf = %s\r\n", buf);
    memcpy(buf, "Hello World", strlen("Hello World"));
    printf("[2]: buf = %s\r\n", buf);

    free(buf);  //释放已经申请成功的内存区域

    return 0;
}

(2.4.2)free函数

#include <stdlib.h>

void free(void *ptr);
功能:
    释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放
    区域的首地址,对同一内存空间多次释放会出错
参数:
    ptr: 需要释放空间的首地址,被释放区应是malloc函数分配的内存区域
返回值:
    无

(3)内存分区代码分析

(3.1)返回栈区地址

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int *fun(void)
{
    int a = 10;
    return &a;  //函数调用完毕,a变量释放
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    p = fun();
    //*p = 100;   //错误,操作野指针指向的内存

    return 0;
}

(3.2)返回data区地址

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int *fun(void)
{
    static int a = 10;
    return &a;  //函数调用完毕,静态变量a不释放
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    p = fun();
    *p = 100;
    printf("*p = %d\r\n", *p);

    return 0;
}

(3.3)值传递1

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void fun(int *temp)
{
    temp = (int *)malloc(sizeof(int));

    *temp = 100;
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    fun(p); //值传递,形参修改不会影响实参
    //printf("*p = %d\r\n", *p);  //错误,操作空指针指向的内存

    return 0;
}

(3.4)值传递2

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void fun(int *temp)
{
    *temp = 100;
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    p = (int *)malloc(sizeof(int));
    fun(p); //值传递
    printf("*p = %d\r\n", *p);  //正确,*p的值为100

    return 0;
}

(3.5)返回堆区地址

简单实例代码如下:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int *fun(void)
{
    int *temp = NULL;

    temp = (int *)malloc(sizeof(int));
    *temp = 100;
    return temp;  //返回堆区地址,函数调用完毕,内存不释放
}

int main(int argc, char *argv[])
{
    int *p = NULL;

    p = fun();
    printf("*p = %d\r\n", *p);

    //堆区空间,使用完毕后需要手动释放
    if (p != NULL)
    {
        free(p);
        p = NULL;
    }

    return 0;
}

18、复合类型(自定义类型)

(1)结构体

(2)共用体(联合体)

(3)枚举

(4)typedef自定义类型

19、文件操作

 

posted @ 2020-12-22 22:40  liangliangge  阅读(2827)  评论(0编辑  收藏  举报