C和指针学习笔记

前言

  本文是《C和指针》这本书籍的学习笔记,虽然我本人并不是C 程序开发者,但我对C 语言一直有莫名的好感,所以买了一些关于C 语言和C++ 语言的书籍来学习下,《C和指针》这本书的内容质量非常不错,我本人也极力推荐,想学习和入门 C 语言的朋友可以读读这本书,大有裨益。

  


  

作者:米开

微信公众号:米开的网络私房菜

image-20200715095229689

  

1 初识C

C 语言是通用的、面向过程的程序设计语言。

B语言

  1969年,美国贝尔实验室的 Ken Thompson 以BCPL语言为基础,设计出很简单且很接近硬件的B语言(取BCPL的首字母)。并且他用 B语言 写了第一个 UNIX 操作系统。

  B 语言是一种无数据类型的语言,所有的数据与机器码相对应,程序员通过内存地址操作内存数据。后来丹尼斯·里奇对B语言进行改造,演化出了至今流行的C 语言。

C语言

  1972年,丹尼斯·里奇和同事在开发Unix系统时需要更好的语言工具,当时没有“高级”语言来更多地控制所有涵盖操作系统的数据。所以在开发Unix操作系统的同时,发明了C语言。C 语言较B语言最重要的改变是引入了数据类型的概念

  1969-1973年在美国电话电报公司贝尔实验室开始了C语言的最初研发。根据C语言的发明者丹尼斯·里奇说,C 语言最重要的研发时期是在1972年

  UNIX之父,C语言之父 丹尼斯·里奇(1941-2011年)

  

image-20201027141823816

C语言环境

需要文本编辑器C编译器就可以开发C语言程序;

C 编译器

GUN gcc 地址: gcc.gnu.org

Window 安装 gcc编译器 MinGW, 地址: mingw.org

C 集成开发工具:

  • code::blocks
  • Visual Studio

code::blocks 是开源免费的工具,特点是运行内存只有30M左右,非常轻量级;

Visual Studio 是微软的商业级开发工具,号称宇宙最强IDE, 比较消耗运行内存(1G甚至更多);

Hello World

学习一门编程语言通常从 Hello World 开始:

#include <stdio.h>

int main()
{
    printf("Hello world!");	
    return 0;
}

注:

  1. 所有的C语言程序都需要包含 main() 函数,代码从main 函数开始执行。
  2. printf() 是打印输出函数,printf() 函数在 <stdio.h> 头文件中声明。
  3. stdio.h 是一个头文件(标准输入输出文件),#include 是预处理指令,用来引入头文件。

2 数据类型

C 语言中有四种数据类型:

  1. 整型
  2. 浮点型
  3. 指针
  4. 聚合类型(数组、结构体、联合体)

注:使用 sizeof 计算数据类型的长度。

整型

整型家族包括 整型、短整型、长整型和字符 。每个都有有符号(signed )和无符号(unsigned )两种类型;

整型:

名称 描述 大小(字节) 范围
short 短整型 2
int 整型 4
long 长整型
char 字符型 1

unsigned 无符号

unsigned short 无符号短整型 8 unsigned int 无符号整型 unsigned long 无符号长整型

整型大小: long > int > short

注:各个数据类型所占空间大小与系统位数有关系,

布尔类型

C语言中没有直接提供布尔类型,使用整数表示真和假。

规则:零是假,非零就是真。

C 规则中没有说真必须是 1,只要是非零的数值(-2、3、3.01)就为真。如下面例子:

if(0){
    printf("0是假");
}
if(95){
    printf("非0就是真");
}

数据类型所占的内存空间大小不用刻意去记, c 可以使用 sizeof 关键字计算数据类型所占内存大小:

sizeof(数据类型/变量)

_Bool类型不需要引用其他库函数,直接就可以使用,

bool类型,则需要 #include <stdbool.h>,C99 加入 bool类型,可以像 C++ 一样去使用bool ;

注:

​ C++中使用 bool 定义布尔值;

枚举

​ 枚举是C语言的基本数据类型。

​ 在C 语言中,枚举中的每一个实例被当做整型来处理,第一个实例的默认值为0,后续成员的值在前一个成员的值上加1。如上面例子,MON = 0,TUE = 1,WED = 2 … 以此类推。

//定义枚举类型
enum DAY{
    MON, TUE, WED, THU, FRI, SAT, SUN
};

​ 也可以手动指定枚举实例的值,从手动指定的位置开始,后续成员的值依然是前一个成员的值上加1,如上面例子,MON = 1,TUE = 2,WED = 3 … 以此类推。

enum DAY{
    MON=1, TUE, WED, THU, FRI, SAT, SUN
};
void test_enum(){
    
    enum DAY day = WED;
    printf("%d\n",day);
}

字符型

名称 描述 大小(字节)
char 字符型 1

​ 字符要用单引号,如 char a = 'a' ;
注:

​ C/C++ 中的字符变量只占用1个字节; 字符型变量并不是把字符本身放到内存中存储, 而是把对应的ASCII编码放到存储单元;

转义字符

\n 换行符

\t 水平制表Tab

\ 代表一个反斜杠”"

浮点型

名称 描述 大小(字节)
float 单精度浮点型 4
double 双精度浮点型 8

科学计数法 float f1 = 3e2; // 3* 10^2 = 300 ; float f2 = 3e-2; // 3* 0.1^2 = 0.03;

字符串(字符数组)

字符串用字符数组表示:

char str[] = "hello word";

指针

聚合类型

数组和结构体

void 类型

字符串

C语言中的字符串实际上是以”\0” 结尾的一维字符数组。

char str[]="Hello";
image-20201122132734165

“Hello” 包含了5个字符,在 str 数组变量中占用6个单位,最后一个字符存储”\0” 结尾字符。等价于下面的声明:

char str[6] = {'H','e','l','l','o','\0'};

字符串处理函数

字符串是最常用的数据类型,C 函数库提供了丰富的字符串操作函数。<string.h> 头文件中定义。

函数 描述
strcpy(s1, s2); 复制
strcat(s1, s2); 拼接字符串
strcmp(s1, s2); 比较两个字符串,如果值相等,返回0; s1>s2, 返回值大于0; s1<s2,返回值小于0;
strlen(s1); 返回字符长度
strchr(s1, ch); 返回一个指针,指向字符串s1中第一次出现字符ch 的位置
strstr(s1, s2); 返回一个指针,指向字符串s1中第一次出现字符串s2 的位置

示例:

变量

作用域

标识符的作用域就是程序中标识符可以被用到的使用区域。

编译器可以区分4种不同类型的作用域:

  • 代码块作用域
  • 文件作用域
  • 原型作用域
  • 函数作用域
int a;
int b(int c);
int d(int e)
{
    int f;
    int g(int h);
    //TODO
    {
        int f,g,i;
    }
    {
        int i;
    }
    return 0;
}

上面是一个标识符作用域示例:

代码块作用域:

​ 位于一对花括号之间的所有语句称为一个代码块。任何被声明在代码块中的标识符都具有代码块作用域,即仅在该代码块范围中有效;

文件作用域:

​ 在代码块之外声明的标识符都具有文件作用域,从标识符声明处到源文件结尾处都是可以访问的。

原型作用域:

​ 原型作用域只适用于函数声明中的形参,函数声明中的形参名称不是必须的,可以省略,但却不可以重复;

函数作用域:

​ 函数作用域只适用于语句标签,语句标签用于 goto 语句,即在一个函数体内语句标签不可以重复。(goto 语句一般不用)

存储类型

​ 变量的存储类型存储变量值的内存类型。变量的存储类型决定了变量何时创建、何时销毁,以及它的值将保持多久。

​ 有三个地方可以存储变量:普通内存、运行时堆栈、硬件寄存器。 这三个地方存储的变量具有不同的特性。

​ 变量的缺省存储类型取决于它的声明位置。凡是在任何代码块之外声明的变量总是存储在静态内存中,不属于堆栈的内存。这类变量称为静态变量,你无法为这类变量指定其他存储类型。静态变量在程序运行前创建,在程序的整个执行期间始终存在。

​ 在代码块内部声明的变量的缺省存储类型是自动的,位于堆栈内存中,称为自动变量。关键字 auto 用来修饰自动变量,不过它很少使用,因为代码块内部声明的变量默认就是自动变量。在程序执行到自动变量的代码块时,自动变量才被创建,当程序执行完离开代码块时,自动变量遍自行销毁。

​ 在代码块内部声明的变量,如果加上 static 关键字,则存储类型变为静态变量。

​ 关键字 register 声明自动变量时,表明它应该存储在机器的寄存器,而不是内存中。这类变量成为寄存器变量。但是编译器并不一定要理睬 register 关键字,也有可能忽略 register 关键字。

注意:

​ 修改变量的存储类型并不代表改变变量的作用域。

指定存储位置的关键字

auto
register
static
extern

auto

auto 是所有局部变量默认的存储类型

void func{
   int mount;
   auto int month;
}

上面定义了两个相同存储类型的局部变量,auto 只能用在函数体内,即auto 只能修饰局部变量。

register

​ register 存储类型定义的变量存储在寄存器而不是内存中。这意味着变量的最大尺寸不能大于寄存器的大小,且不能对它进行取址运算 &,因为它没有内存位置。

void func{
   register int age;
}

注:

​ register 关键字修饰的变量并不意味着一定在寄存器上分配,这取决去硬件和实现的限制。

static

static 修饰的变量在程序整个生命周期内都存在。

static 可以修饰局部变量,全局变量和函数;

extern

常量

整数常量:

​ 整数常量可以有前缀和后缀,没有前缀表示十进制整数,0 表示八进制,0x 表示十六进制;后缀可以是U和L,U 表示无符号,L 表示长整型,后缀的U和L不区分大小写,不区分先后顺序,U和L可以组合使用,如 UL 表示无符号长整型;但不可以重复,如UU 或LL;

//十进制整数
99
//八进制整数
017
//十六进制整数
0x6D60D2CB4093F6A47
//无符号整数
128u
//长整型
987654320L   

浮点常量

浮点型的字面值在缺省的情况下都是double 类型的。加上 L 或 l 后缀表示 long double 类型,加上f 后缀表示 float类型。

//double 类型
3.14
//long double 类型
3.1415L
//float 类型
3.14f  

字符常量

定义常量

在C 中,有两种定义常量的方式:

  • 使用 const 关键字
  • 使用 #define 预处理器
#include <stdio.h>
 
#define LENGTH 10  //预处理器定义常量
#define WIDTH  5
 
void fun()
{
   int area = LENGTH * WIDTH;
   const long HEIGHT = 180;	//const 关键字定义常量
}

规范:

常量名一般定义为全大写字母。

3 运算符

3.1算数运算符

运算符 术语 示例 结果
+ 正号 +3 3
- 负号 -3 -3
+
-
*
/
% 取余 10%3 1
++i 前置递增 a=2;b=++a; a=3;b=3;
i++ 后置递增 a=2;b=a++; a=3;b=2;
–i 前置递减 a=2;b=–a; a=1;b=1;
i– 后置递减 a=2;b=a–; a=1;b=2;

3.2赋值运算符

简单赋值运算符和复合赋值运算符

运算符 术语 示例 结果
= 赋值
+= 加等于
-= 减等于
*= 乘等于
/= 除等于
%= 模等于

3.3比较运算符

运算符 术语 示例 结果
== 等于
!= 不等于
> 大于
>= 大于等于
< 小于
<= 小于等于

3.4逻辑运算符

运算符 术语 示例 结果
||
&&
!

4 流程控制

C 语言支持3种基本的程序运行结构: 顺序结构, 选择结构, 循环结构

选择结构

if-else 语句

int num = 600;
if(num > 600){
    printf("数量大于600");
}else if(num > 300){
    
}else{
    
}

switch 语句

int num = 600;
switch(num){
    case 100:
        printf("数量为100");
        break;
    case 600:
        cout << "yyy" <<endl;
        break;
    default:
        cout << "zzz" <<endl;
}

三目运算符

由 ? 和 : 组成的条件判断表达式,问号前面是逻辑表达式,如果为true,返回结果一,否则返回结果二;

int num = 600;
int score = num > 600 ? 600 : 0;

循环结构

循环语句有3个,配合跳转关键字continue, break 使用。

break 语句用于跳出整个循环结构;

continue 跳出本次循环,继续下一次循环;

while语句

int i = 0;
while(i<10){
   i++; 
}

while 括号中的表达式成立,才会执行语句;

do while语句

int i = 0;
do{
  i++;
}while(i<10)

do while 语句是先执行do 代码块中的语句,再判断while语句;至少执行1遍;

for 循环

for(int i = 0; i<100; i++){
  
}

for语句有3个表达式

表达式一:初始化语句,只执行一次;

表达式二:循环判断语句,条件满足,执行循环体,否则循环结束;

表达式三:每次循环结束后需要执行的表达式;

如果表达式二省略,即不判断循环条件,那么这是个死循环:

for(;;){
  
}

死循环中如果遇到 break,一样可以跳出循环体。

continue 和 break

continue 和 break 在循环体中控制流程:

  • continue:结束本次循环,开始下一次循环;
  • break:结束整个循环体;

goto 灵活跳转

goto FLAG;
	 cout << "xxx" <<endl;
FLAG:
	 cout << "zzz" <<endl;

注:

​ 因为 goto 控制逻辑过于灵活,如果在程序中大量使用,代码将会难以调试和阅读,因此在程序中尽量减少使用,甚至是不用。

5 数组

数组中存储一个固定大小相同数据类型顺序集合

语法:

//数据类型 数组名 [数组大小] 
type arrayName [ arraySize ];

一维数组

//定义并初始化数组
int num[3] = {1,2,3};
//初始化时如果不指定数组大小,默认为初始化时元素的个数
int num[] = {1,2,3};
//通过索引下标访问数组指定元素
int a = num[0];
//给数组的指定元素赋值
num[2] = 9;

注:数组下标从0开始,最大值为数组元素个数-1。

多维数组

多维数组实际上是一维数组的一种特例,就是多维数组每个元素本身也是一个数组。

二维数组是多维数组中最简单的一种,二维数组可以用来表示矩阵。

数据类型 数组名 [行数] [列数] ;

//定义方式一:
int array[2][3];  	//行数,列数
[0][0] = 1;			//第一行,第一列
[0][1] = 2;			//第一行,第二列
[0][2] = 3;
[1][0] = 4;
[1][1] = 5;
[1][2] = 6;

//定义方式二:(也是最常用的)
int array2[2][3]={
    {1,2,3},
    {4,5,6}
}; 

for(int i=0;i<2;i++){
    for(int j=0;j<3;j++){
    	cout<< array[i][j] <<endl;
	}
}

注:

​ 我们通常把二维数组描述为行和列,其实这是从逻辑上理解和划分的;实际上一维数组和多维数组在数据存储上没有任何区别,都是线性排列的顺序结构。

在绝大多数表达式中,数组名的值就是指向数组第1个元素的指针。这个规则有两个例外,sizeof 返回整个数组所占的字节数而不是一个指针所占字节数;取址符 & 返回的是一个指向数组的指针,而不是一个指向数组第1个元素的指针的指针。

​ 当数组名作为函数参数传递时,实际传给函数的是一个指向数组第1个元素的指针。函数所接受到的是原参数的一份拷贝,所以函数对其进行操作不会影响实际的参数。但是,对指针执行间接访问操作就可以修改原先数组的元素的值。数组形参既可以声明为数组,也可以声明为指针,这两种声明形式只有当它们作为函数形参时才是相等的。

​ 数组和指针并不相等。当我们声明一个数组时,同时也分配了一些内存空间,用于容纳数组元素;但当我们声明一个指针时,它只分配了容纳指针本身的空间。

6 函数

C 语言中,一个函数包含四部分:

  1. 返回值类型;
  2. 函数名
  3. 参数
  4. 函数体

下面是一个函数示例:

int max(int num1, int num2) 
{
   int result;
   if (num1 > num2)
      result = num1;
   else
      result = num2;
   return result; 
}

函数的声明

int switch0(int num1,int num2);

函数的定义

int switch0(int num1,int num2){
    int temp = num1;
    num1 = num2;
    num2 = temp;
}

参数传递方式

C 的规则很简单,所有的参数都是值传递。

​ 值传递意味着复制一份实参的值,这样当修改形参的值时,才不会影响实参的值。当参数是一个数组时,并且在函数体内使用数组下标对数组元素进行修改操作,那么数组元素确实被修改了。表面上看这和值传递的规则相悖,但其实不然,数组名其实也是一个指针,传递数组名其实复制的是指针的值,从而间接的修改了数组元素的值。下标引用其实是间接访问的另一种形式罢了。

函数参数传递的方式有2种:

  1. 值传递 : 形参的值发生改变, 实参不会改变;
  2. 地址传递 : 形参的值发生改变, 实参也会改变;

预处理器

​ C 预处理器不是编译器的组成部分,但它是编译过程中一个单独的步骤。C 预处理器是在实际编译之前由编译器调用的独立程序。

所有的预处理器命令都是以井号(#)开头,它必须是第一个非空字符,一般预处理器命令从第一行开始。

常用预处理指令:

指令 描述
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#if 条件编译的判断语句
#endif 条件编译的结束语句

#define

#define NAME VALUE

有了这条指令后,有符号 NAME 出现的地方,预处理器就会把它替换成 VALUE。

头文件

.h 文件

  • 编译器中自带的头文件(标准库);
  • 用户自定义的头文件;

指针

指针是C 语言的灵魂,是C 语言经久不衰和广泛流行的一个重要原因。

基本概念

指针本质上是一个变量,保存的是一个内存地址。 换句话说,指针只是内存地址的别名罢了。

指针的作用:可以通过指针直接或间接的访问内存数据;

  • 内存地址是从0开始记录的, 一般用十六进制数字表示;
  • 可以利用指针变量保存地址;

声明/初始化指针

声明指针的语法: *数据类型 指针变量名

int *p;			//声明指针: 数据类型 *指针变量名

变量取址的语法: &变量名;

int a = 10;		
int *p = &a;		//声明指针并把变量a的内存地址并赋值给指针p

特别说明:

int *p 很容易写成 int* p ,两者的写法具有相同的意思。但 int* p 并不是规范的写法,有时候容易造成误解,比如 int* p,q,r; 这个语句的声明,容易理解为声明了 p、q、r 三个指向int类型的指针,但事实并非如此,星号其实只作用于 p , 右边的q、r 其实只是普通变量,正确声明三个指针的语法是 int *p,*q,*r;

解引用指针

对指针进行解引用操作可以间接获取指针所指向的值。

解引用语法: *** 指针变量名**;

int b = *p; 	//解引用指针

注:

int *p 中,声明的int 数据类型,不是说明指针 p 是 int 类型,而是指针 p 指向的内存地址存放的数据是int 类型。

声明一个指针变量并不会自动分配任何内存。对指针进行解引用(间接访问)之前,必须对指针进行初始化,要么使它指向现有的内存,要么给它分配动态内存。对未初始化的指针变量执行解引用操作是非法的,而这种错误往往很难发现,其结果常常是一个不相关的值被修改!这种错误也难以调试。

指针所占内存空间

所有指针变量占用固定的空间大小。64位操作系统下, 指针占用 8个字节 ,32位操作系统下, 指针占用 4个字节

​ 也就是说对于 int *along long *b ,指针a 和 指针b 所占用的内存空间大小是一样的,唯一的不同是它们所指向的变量的数据类型是不同的,指针 a 解引用后是 int 类型的数据,指针 b 解引用后是 long long 类型的数据。

指向指针的指针

​ 我们平时所说的变量是普通变量,平时所说的指针记录了变量的内存地址;然而指针其实也是变量,也有自己的内存地址,记录指针变量内存地址的指针就是指向指针的指针(有点绕口,其实很好理解)。

int a = 10;
int *p = &a;
int **p2 = &p;

指针 p 指向变量 a 的地址,指针p2 指向了指针变量 p 的地址,

NULL 指针

在指针声明的时候,如果没有确切的地址可以赋值,为指针赋值为NULL是个好习惯。

//指针赋值为NULL
int *p = NULL;

注:

​ 空指针指向内存地址为0的位置,为内存位置零并没有存储任何变量,所以NULL 指针并未指向任何东西。因此对NULL 指针解引用是非法操作!

野指针

野指针: 指针变量指向非法的内存空间;

int *p3 = (int*)0x1100;	//野指针

注意:

​ 空指针和野指针都不是自己分配的内存,所以都不能访问!

指针与目标变量是密切相关的,

不能给指针变量赋值自定义值(如 int* p = 0x1000),给指针变量赋的值必须是合法的内存地址。

const 修饰指针

const 修饰指针的三种情况:

  • 常量指针
  • 指针常量
  • 全常量指针

常量指针

const 直接修饰指针: const int *p ,叫常量指针;

特点:

指针的指向的值不可以改, 指针的指向可以改;

int a = 10;
int b = 20;

const int *p = &a;
*p =20; //错误写法: 指针指向的值不可以改;
p = &b; //正确写法

指针常量

指针常量对应指针变量,const 直接修饰常量: int * const p , 叫指针常量;

说明:

指针的指向不可以改, 指针的指向的值可以改;

int a = 10;
int b = 20;

int * const p = &a;
*p = 20; //正确写法
p = &b; //错误写法: 指针的指向不可以修改;

全常量指针

指针的指向和指向的值都不可以改!

int a = 10;
int b = 20;

const int * const p = &a;
*p =20; 	//错误写法
p = &b; 	//错误写法

技巧:

  1. const 翻译为常量, int* 翻译为指针,所以 const int* p 叫常量指针; int* const p 叫指针常量;

  2. const 修饰谁,谁就不能改;
    const int* p 修饰的是*, 那么 *p (指针的解引用的值)就不能改;

    int* const p修饰的是p, 那么 p (指针的引用)就不能改;

指针运算

C 指针的算数运算只限于两种:

  • 指针 + - 整数
  • 指针 - 指针

注:指针变量只支持加法和减法,不支持乘法、除法和取余运算!

​ 数组中的元素存储在连续的内存位置中,后面元素的地址大于前面元素的地址。因此,在数组中对一个元素的指针加1就使他指向数组中下一个元素,加5就向右移动5个位置,减3就向左移动3个位置。对指针加减整数的结果类型也是指针。

​ 如果对指针执行加减法后指向数组第一个元素的前面或在数组最后一个元素的后面,那么返回值就是未定义、不确定的。

//指针在数组中的算数运算:指针 +- 整数
int arr_a[4]={8,9,11,4};
int *p0 = arr_a;
int *p1 = p0+2;	//p0指针向右移动2个单位, *p1 的值为11
int *p3 = p0-1;	//p3指针指向了一个非法位置,其返回值是未定义的

只有当两个指针都指向同一个数组的元素时,才允许两个指针相减。两个指针相减的运算结果是一个有符号的整数。结果是两个指针之间的内存距离(单位是指针所指向的数据类型的长度,而不是字节)。例如指针p1指向array[i] ,指针p2指向array[j] ,那么 p2 - p1 就等于 j - i 的值。

//指针在数组中的算数运算:指针 - 指针
int arr_a[4]={8,9,11,4};
int *p0 = arr_a;
int *p3 = p0 +3;	
int diff = p3 - p0;//diff 的值为3;

总结:

​ 可以看到,无论是 指针+-整数 还是 指针 - 指针,只有在数组中才具有意义。

​ 很多地方、很多人都说正确操作指针不是容易的事,有时更是件危险的事。这句话怎么理解呢?事实上,绝大多数编译器不会检查指针表达式的结果是否位于合法的边界内,因此,程序员应当负起这个责任。编译器也不会阻止你取一个标量的地址并对其进行指针运算,即使它无法预测运算结果所产生的的指针将指向哪个变量(这就是操作指针的危险之处了)。指针越界和指向未知的值是两个常见的错误,当你使用指针运算时,务必非常小心,确保指针的运算结果指向有意义的东西。

指针和数组

我们知道可以用数组下标来访问数组元素;

利用指针来访问数组元素;

int arr[10] = {1,2,3,4,5,6,7,8,9,10};	//初始化数组
int a = arr[0];		//使用数组下标访问数组元素

int *p = arr;		//数组名就是数组首个元素的地址
int b = *p;			//解引用

for(int i =0;i<10;i++){
    cout << arr[i] <<endl;
    cout << *p <<endl;	//指针解引用,找到指向的内存数据;
    p++;				//指针后移;
}

指针和函数

//值传递
int a =10;
int b =20;

void swap1(int a,int b){
    int temp = a;
    a = b;
    b = temp;
}
swap1(a,b);
cout << a <<endl;
cout << b <<endl; //结果a=10, b=20;

//地址传递
void swap2(int* p1,int* p2){
    int temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}
swap2(&a,&b);
cout << a <<endl;
cout << b <<endl; //结果a=20, b=10;

结构体

数组中存放的是都是相同类型的数据,而在结构体中,可以存放不同数据类型的值。

结构体的定义和使用

结构体是C 语言中自定义的数据类型 ,使用 struct 关键字来定义结构体

struct Student{
	int a;
	char b;
	double c;
};

注:

​ 最后的分号不可以省略,定义结构体时,结构体内的属性不可以赋值(如:int age = 18),否则非法报错;定义结构体不会分配内存,创建结构体变量才会分配内存。

2种方式创建结构体变量

//方式一:
Student stu = {1,"wyc",18};

//方式二:
Student stu ;
stu.id = 1;
stu.name = "wyc";
stu.age = 24;

printf(stu.name);

​ 使用方式一有一个缺点,那就是结构体成员的初始化顺序必须和声明时保持一致,如果一个成员不赋值,那么后面的所有成员都不能赋值;

注:

​ 定义结构体时, struct 关键字不可以省略;

​ 创建结构体变量时, struct 关键字可以省略;

​ 结构体中可以声明成员函数,但很少这么做,一般只会声明成员变量;

结构体数组

//创建结构体数组
Student stus[3]{
	{1,"wyc",18},
	{2,"www",19},
	{3,"jack",25}
};

结构体指针

//1.创建结构体变量
Student stu = {1,"wyc",18};
//2.通过指针指向结构体变量
Student *p = &stu;
//3.通过指针访问结构体变量中的数据 
int id = p->id;
string name = p->name;
int age = (*p).age;

注:

​ 通过指针访问结构体变量中的数据有两种方式:p->name(*p).name ;它们都等价于结构体变量.属性 stu.name

结构体嵌套结构体

struct Student{
	int id;
	string name;
	int age;
};

struct Teacher{
	int id;
	string name;
	int age;
    Student stu;
};

int main(){
    Teacher teacher;
    teacher.id = 001;
    teacher.name = "jack";
    teacher.age = 39;
    
    teacher.stu.id=020;
    teacher.stu.name="wyc";
    teacher.stu.age = 24;
}

结构体做函数参数

函数的参数传递方式有2种: 值传递 和 地址传递;

Student stu = {01,"jack",18};

//值传递
void printStudent01(Student stu){
    stu.age = 28;
    cout << stu.age << endl;
}

//地址传递
void printStudent02(Student *stu){
    stu->age = 28;
    cout << stu->age << endl;
}

printStudent01(stu);
printStudent02(&stu);

​ 结果同上面的基本数据类型一样, 值传递时, 作为形参的结构体属性发生变化, 实参不会变化;

而地址传递, 形参改变, 实参也会改变;

总结

​ 结构体变量使用点号操作符 (.) 访问结构体的成员;结构体指针使用箭头操作符 (->) 访问结构体的成员;

​ 结构体的成员可以是基本数据类型、数组、指针或者结构体。但结构体不能包含也是这个结构体的成员,它的成员可以是指向这个结构体的指针。这种技巧常常用于用于链式数据结构。

​ 结构体可以作为参数传递给函数,也可以作为返回值从函数返回。但向函数传递一个结构体指针往往效率更高。使用 const 声明为常量结构体指针可以防止函数修改结构体数据。

联合体

共用体/ 联合体

union

一个联合体的所有成员都存储在同一个内存位置。

内存管理

C 函数库提供了几个内存分配和管理的函数,这些函数在 <stdlib.h> 头文件中被定义。

函数 描述
void *malloc(int num); 分配一块指定大小的连续内存空间(未初始化)
void *calloc(int num, int size); 分配一块num个size大小的连续内存空间(共num*size),并初始化内存;
void free(void *address); 释放指针指向的内存空间
void *realloc(void *address, int newsize); 重新分配内存空间大小

注:void * 表示未确定类型的指针,C/C++ 规定void * 类型可以强制转化为任意类型的指针。

动态分配内存(malloc 和 calloc )

alloc 是 allocate 的缩写,动词分配的意思。

malloc

malloc 函数用于动态分配一块连续的内存,不会分配两块或多块不连续的内存。

calloc

calloc 较 malloc 的区别在于,calloc 分配完内存后,返回指向内存的指针之前会将内存自动初始化为0,这个初始化常常带来方便。

释放内存(free)

free

​ 当一个动态分配的内存块不再使用时,应该调用 free 函数释放内存,动态内存释放后不能再访问。传递给 free 的指针必须是一个从 malloc 、calloc 和 realloc 函数返回的指针;使用free 函数释放一块不是动态分配的内存可能导致程序错误或立即终止。

内存泄漏(Memory Leak)

内存泄漏是指动态分配内存后,当它不再使用时未被释放。内存泄漏会在增加程序的体积,有可能导致系统崩溃。

修改内存大小(realloc)

realloc

realloc 函数用于修改一个原先已分配的内存块大小,使用 realloc 可以扩大或缩小原先分配的内存块大小。

当缩小内存时,该内存块尾部的部分被释放掉,剩余的部分内容依然保留。

当扩大内存时,如果原先的内存块后面有足够的空间可以分配,则直接扩张;如果不够用,则重新分配一块连续的内存块,原先的数据被复制过来。因此使用 realloc 后,你不能再使用旧的指针,而应该使用 realloc 函数返回的新指针。

分配内存错误

分配内存最常见的错误就是忘记检查所请求的内存是否分配成功。

内存泄漏

​ 当动态分配的内存不再需要使用时,它应该被释放,这样它以后才可以被重新分配使用。动态分配的内存使用后不释放将引起内存泄漏()。内存泄漏会一点点榨干可用内存空间。最终导致内存占满,想要摆脱这个困境,只能杀掉进程重启程序或者重启服务器。

总结

​ 声明数组时,必须在编译时指定它的长度。动态内存分配允许程序为一个长度在运行时才确定的数组分配内存空间。

​ malloc 和 calloc 函数都用于动态分配一块连续的内存,并返回一个指向该块内存的指针。malloc 的参数就是要分配内存的字节数。calloc 的参数指定元素的个数和每个元素所占字节长度。calloc 函数在返回前把内存初始化为0,而 malloc 函数返回时并未初始化内存。

​ 调用 realloc 函数可以修改一块已经动态分配的内存大小。增加内存时有可能新开辟一块更大的内存,然后把原内存块的所有数据复制过来,并返回新的指针。

​ 如果请求的内存分配失败,malloc 、calloc 和 realloc 函数返回的是一个NULL 指针。

​ 当一个动态分配的内存块不再使用时,应该调用 free 函数释放内存,动态内存释放后不能再访问。传递给 free 的指针必须是一个从 malloc 、calloc 和 realloc 函数返回的指针;使用free 函数释放一块不是动态分配的内存可能导致程序错误或立即终止。

​ 使用 sizeof 计算数据类型的长度,可以提高程序的可移植性。

IO 函数

Unix I/O

Unix I/O 会使用以下几个函数

//打开文件. 若成功返回新的文件描述符,若错误返回-1
int open(char *filename, int flags, mode_t mode);
//关闭文件. 若成功返回0,若错误返回-1
int close(int fd);
//读取数据到内存. 若成功则为读的字节数,若失败则为-1
ssize_t read(int fd, void *buf, size_t n);
//写入数据到外部. 若成功则为写的字节数,若失败则为-1
ssize_t write(int fd, const void *buf, size_t n);
//手动定位到文件的某个位置
lseek

注:

​ ssize_t 被定义为有符号的long 类型,即 signed long , 因为返回值可能存在 -1; size_t 被定义为无符号的 long类型 unsigned long;

Unix I/O 函数被称为不带缓存的I/O,不带缓存意味着每一次函数调用都会调用内核的系统调用。

在内核中,每一个打开的文件都用文件描述符表示。

标准 I/O

​ C 语言定义了一组高级别的输入输出函数,称为标准I/O 库。

​ 为程序员提供了 Unix I/O 的较高级别的替代,标准I/O 库提供了打开和关闭文件的函数(fopen 和 fclose),读写字节的函数(fread 和 fwrite),读写字符串的函数(fgets 和 fputs),以及复杂格式化的I/O函数(scanf 和 printf)。

标准 I/O 库是基于Unix I/O 实现的。提供了一组强大的高级I/O 接口。对于大多数应用程序而言,标准I/O 更简单,是优于 Unix I/O 的选择。

流和文件 FILE

​ **标准I/O 库将一个打开的文件抽象化为一个流。 **流(指向 FILE 结构的指针)是对文件描述符和流缓冲区的抽象。流缓冲区的目的是使开销较高的 Unix IO 系统调用的次数尽可能的减少,提供系统效率。

​ 举个栗子,一个程序反复调用标准 I/O 的 getc 函数,每次调用返回文件的下一个字符。当第一次调用getc 时,I/O 库通过调用一次 read 函数来填充流缓冲区,然后将流缓冲区的第一个字符返回给应用程序,只要缓冲区还有未读的字节,接下来对getc 的多次调用都是直接从流缓冲区中读取数据,而不是系统调用。

​ FILE 结构是在 stdio.h 中定义的,不要把 FILE 理解为磁盘上的数据文件,它是一个结构体,用于访问一个流。每一个流都有一个相应的 FILE 与之关联。

​ 默认情况下, I/O流操作是进行缓冲的,绝大多数的IO操作都是完全基于缓冲的。C 程序打开文件时,将文件的全部内容或部分内容加载到缓冲区,并返回一个指向 FILE 结构的指针,接下来对文件的所以操作,都是基于缓冲的。

stdin | stdout | stderr

对于程序员而言,一个流就是一个指向 FILE 类型的指针。每个ANSI C 程序开始时默认都会打开3个流: stdin, stdout ,stderr ,分别对应标准输入,标准输出和错误输出。

#define stdin  (__acrt_iob_func(0))
#define stdout (__acrt_iob_func(1))
#define stderr (__acrt_iob_func(2))

​ C 语言同样为程序打开的3个标准流提供相应的 I/O 函数,printf 用于向标准输出流中写入信息,perror 函数用于向错误输出流中写入信息,scanf 用于向标准输入流中写入信息;因为3个标准流都是默认打开的,所以使用 printf 、scanf 等函数操作标准流不需要使用 fopen 函数再打开流。

//打印错误信息
void perror(char const *message);

注:

​ 在网络套接字上不要使用标准I/O 函数来进行输入输出。

打开和关闭流

fopen 函数用于打开流,并把一个流与这个文件相关联.

//打开流
FILE *fopen(char const *name, char const *mode);

​ 两个参数都是字符串。name 是打开的文件或设备的名字。mode 参数用于指定流是只读、只写还是可读可写,以及它是字符流还是字节流。下面列出mode 常用格式

读取 写入 添加
文本 “r” “w” “a”
二进制 “rb” “wb” “ab”

mode 以 r 、w 或 a 开头,分别表示打开的流用于读取、写入还是添加。如果一个文件打开是用于读取的,那么它必须是原先已经存在的;如果一个用于写入的文件原先已经存在,那么原来的内容将会被删除,也就是覆盖写入,如果原先不存在,那么就会创建一个新文件;如果一个用于添加的文件原先就存在,那么原先的内容并不会删除,会以追加的方式写入,如果原先不存在,那么将会创建。无论是那种情况,数据只能从文件的尾部写入。

 如果 fopen 函数执行成功,则返回一个指向 FILE结构的指针,代表新创建的流。如果执行失败,就会返回一个 NULL 指针。

特别注意:

应该始终检查 fopen 函数的返回值!如果函数失败,会返回一个NULL值。

fclose 函数用于关闭流:

//关闭流
int fclose(FILE *f);

​ 对于输出流,fclose 函数在关闭流之前会刷新缓冲区。如果执行成功,则返回0,否则返回 EOF。

注:常量值 EOF(End Of File)表示文件结尾的意思。

字符I/O

//从指定流中读取单个字符
int fgetc(FILE *stream);
int getc(FILE *stream);
//从标准输入流中读取单个字符
int getchar(void);

​ 需要操作的流作为参数传递给 getc 和 fgetc,而 getchar 只能从标准输入流中读取。每个函数从流中读取下一个字符,并把它作为函数返回值进行返回。如果流中没有更多字符,函数就返回常量值 EOF。

​ 注: 虽然是从输入流中读取字符,但返回值却不是字符类型 char ,而是整型 int.

//把单个字符写入到指定流中
int fputc(int character, FILE *stream);
int putc(int character, FILE *stream);
//把单个字符写入到标准输出流中
int putchar(int character);

注:

​ fgetc 和 fputc 是真正的函数,但 getc、getchar与 putc、putchar 都是通过 #define 指令定义的宏。这个区别实际上不必太看重,结果相差甚微。

撤销字符I/O

当逐个读取字符时, 你通常不知道下一个字符是什么, 如果读到的字符不符合你的预期, 而又不想丢弃,则可以使用 ungetc 退回到字符流中.

int ungetc(int character, FILE *stream);

未格式化的行I/O (字符串)

//从指定流中读取字符并复制到buffer中;
char *fgets(char *buffer, int buffer_size, FILE *stream);
char *gets(char *buffer);

//从标准输入流中读取字符
int fputs(char const *buffer, FILE *stream);
int puts(char const *buffer);

​ fgets 从指定流中读取字符并复制到buffer中。当它读到第一个换行符并存储到缓冲区,或缓冲区内的字符数达到 buffer_size-1 个时,就会停止读取。 这种情况下,并不会丢失数据,因为下次调用fgets 将从流中的当前位置开始读取。

二进制I/O

//从指定流中读二进制数据
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
//写二进制数据到指定流中
size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream);

​ buffer 是一个指向用于保存数据的内存位置的指针,size 是缓冲区每个元素的字节数,count 是读取或写入的元素数,stream 是输入流或输出流。函数的返回值是实际读取或写入的元素的个数,而并非字节数。

​ 字节流要比字符流操作效率高,因为每个值的位直接从流中读取或向流中写入,不需要任何转换。

刷新和定位函数

刷新函数很有用,在输出流中调用 fflush 会立即把缓冲区内的数据写出,不管它是否已经写满。

int fflush(FILE *stream);

​ 调用 fflush 函数可以确保调试信息实际打印出来, 而不是保存在缓冲区中直到以后才打印.

​ 默认情况下,IO流是顺序读取的。但是你可以通过在读取或写入之前定位到一个不同的位置实现随机IO操作。fseek 函数允许你指定文件中的一个位置,它用一个偏移量表示。ftell 函数返回文件的当前位置。rewind 函数返回到文件的起始位置。

//返回流的当前位置
long ftell(FILE *stream);
//定位到流的某个位置
int fseek(FILE *stream, long offset, int from);
//定位到流的起始位置
void rewind(FILE *stream);

​ ftell 函数返回流的当前位置,这个位置是下一个读取或写入的位置与文件起始位置的偏移量.在字节流中,这个值就是当前位置与起始位置之间的字节数。

​ fseek 的 offset参数表示偏移量,很好理解,from 参数有三个可选值:SEEK_SET、SEEK_CUR、SEEK_END,分别表示从流的起始位置开始偏移, 从流的当前位置开始偏移,以及从流的尾部位置开始偏移。offset 可正可负, 正值表示向尾部方向偏移, 负值表示向起始位置偏移。

​ rewind 函数则直接定位到流的起始位置。

改变缓冲方式

标准I/O提供三种类型的缓冲方式:

  • 全缓冲
  • 行缓冲
  • 不缓冲
//
void setbuf(FILE *stream, char *buf);
//
int setvbuf(FILE *stream, char *buf, int mode, size_t size);

​ 注意,setbuf 和 setvbuf 这两个函数只有当指定的流打开但还没有进行任何其他操作的情况下才能被调用。

​ setbuf 设置了另一个数组,用于对流进行缓冲,这个数组的字符长度必须为 BUFSIZ (它在stdio.h 中定义)。为一个流手动指定缓冲区会防止IO库为它动态分配一个缓冲区,如果buf 设置为 NULL,那么将关闭该流的所有缓冲方式。

​ setvbuf 函数更为通用, mode 参数用于指定缓冲的类型。_IOFBF 指定一个完全缓冲的流; _IONBF 指定一个不缓冲的流; _IOLBF 指定一个行缓冲的流; 所谓行缓存,是指每当一个换行符写入到缓冲区时,缓冲区便进行刷新。

临时文件

tempfile 函数返回一个与一个临时相关联的流,当流关闭后, 这个文件被自动删除.

FILE *tmpfile(void);

C 标准库

C 标准库中有些函数是使用汇编语言编写的,用于处理更加底层的操作(硬件)。

<stdio.h>

stdio.h 是标准输入输出头文件

printf()

//打印字符串常量
printf("string");
//打印十进制整数
printf("a: %d",a);

printf() 常用格式代码:

格式 含义
%d 以十进制形式打印一个整型值
%o 以八进制形式打印一个整型值
%x 以十六进制形式打印一个整型值
%s 打印字符串
%c 打印字符
%f 打印浮点数
%g 打印浮点数,去除无意义的0
%p 打印指针

<stdlib.h>

<string.h>

<math.h>

<time.h>

<errno.h>

README

作者:米开

微信公众号:米开的网络私房菜

版权声明:本文遵循知识共享许可协议3.0(CC 协议): 署名-非商业性使用-相同方式共享 (by-nc-sa)

参考:

  《C 和指针》

  菜鸟教程【C语言教程】

  C 语言教程

修改记录:

  2020-10-27 米开 第一次修订

  


C和指针学习笔记
http://jackpot-lang.online/2020/10/27/C_C++学习/C 和指针学习笔记/
作者
Jackpot
发布于
2020年10月27日
许可协议