C语言基础全总结

我学习的第一门编程语言是Java,所以之前一直没有系统学习过C语言。这篇文章主要就是我学习过程的一个总结,方便以后复习查看。

一. C语言初识与简单介绍

1972年,贝尔实验室的丹尼斯·里奇(Dennis Ritch )和肯·汤普逊(Ken Thompson )在开发UNIX操作系统时设计了C语言。然而,C语言不完全是里奇突发奇想而来,他是在B语言(汤普逊发明)的基础上进行设计。至于B语言的起源,那是另一个故事。C语言设计的初衷是将其作为程序员使用的一种编程工具,因此,其主要目标是成为有用的语言。

在过去40多年里,C语言已成为最重要、最流行的编程语言之一。它的成长归功于使用过的人都对它很满意。过去20多年里,虽然许多人都从C语言转而使用其他编程语言(如,C++、Objective C、Java等),但是C语言仍凭借自身实力在众多语言中脱颖而出。在学习C语言的过程中,会发现它的许多优点(见图1.1)。下面,我们来看看其中较为突出的几点。
在这里插入图片描述
C是高效的语言。在设计上,它充分利用了当前计算机的优势,因此C程序相对更紧凑,而且运行速度很快。实际上,C语言具有通常是汇编语言才具有的微调控制能力(汇编语言是 为特殊的中央处理单元设计的一系列内部指令,使用助记符来表示;不同的CPU系列使用不同的汇编语言),可以根据具体情况微调程序以获得最大运行速度或最有效地使用内存。

虽然这些年来C++、python和JAVA非常流行,但是C语言仍是软件业中的核心技能。在最想具备的技能中,C语言通常位居前十。特别是,C语言已成为嵌入式系统编程的流行语言。也就是说,越来越多的汽车、照相机、DVD播放机和其他现代化设备的微处理器都用C语言进行编程。

使用VS创建并运行一个hello world!
1.打开VS,点击创建新项目,来创建一个空项目。
在这里插入图片描述
2.输入项目名称,选择好目录后点击创建。
在这里插入图片描述
3.右击源文件、之后点添加新建项
在这里插入图片描述
4.创建HelloWorld.c
在这里插入图片描述
5.编写如下代码,并点击**开始执行(不调试)**按钮。
在这里插入图片描述
在这里插入图片描述
这是程序的第1行。#include <stdio.h> 的作用相当于把stdio.h 文件中的所有内容都输入该行所在的位置。实际上,这是一种“拷贝- 粘贴”的操作。include 文件提供了一种方便的途径共享许多程序共有的信息。所有的C编译器软件包都提供stdio.h 文件。该文件中包含了供编译器使用的输入和输出函数(如,printf() )信息。该文件名的含义是标准输入 /输出头文件 。通常,在C程序顶部的信息集合被称为头文件。
int main() 是主函数,程序从这里开始执行。

到这里,就完成了任何一门语言学习的第一步。


二. 数据类型及语句

1. C语言的注释

多行注释:一种是以/*开始、以*/结束的块注释;

/*
这是
注释
*/

单行注释:一种是以//开始、以换行符结束的单行注释

//这是注释

2. C语言数据类型和关键字

基本数据类型

C语言的基本数据类型为:整型、字符型、实数型。这些类型按其在计算机中的存储方式可被分为两个系列,即整数(integer)类型和浮点数(floating-point)类型。
这三种类型之下分别是:shortintlongcharfloatdouble这六个关键字再加上两个符号说明符signedunsigned就基本表示了C语言的最常用的数据类型。
下面列出了在32位操作系统下常见编译器下的数据类型大小及表示的数据范围:

类型名称类型关键字占字节数其他叫法表示的数据范围
字符型char1signed char-128 ~ 127
无符号字符型unsigned char1none0 ~ 255
整型int4signed int-2,147,483,648 ~ 2,147,483,647
无符号整型unsigned int4unsigned0 ~ 4,294,967,295
短整型short2short int-32,768 ~ 32,767
无符号短整型unsigned short2unsigned short int0 ~ 65,535
长整型long4long int-2,147,483,648 ~ 2,147,483,647
无符号长整型unsigned long4unsigned long0 ~ 4,294,967,295
单精度浮点数float4none3.4E +/- 38 (7 digits)
双精度浮点数double8none1.7E +/- 308 (15 digits)
长双精度浮点数long double10none1.2E +/- 4932 (19 digits)
长整型long long8__int64-9223372036854775808~9223372036854775808
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	int a = 2, b = 1;//定义两个变量a,b并赋值
	int c;//定义变量c用于计算a+b的和
	c = a + b;
	printf("a+b = %d",c);//输出结果a+b=3
	return 0;
}

C语言关键字
在C语言中,为了定义变量、表达语句功能和对一些文件进行预处理,还必须用到一些具有特殊意义的字符,这就是关键字,我们用户自己定义的变量函数名等要注意不可以与关键字同名。

C语言中的32个关键字
autodoubleintstruct
breakelselongswitch
caseenumregistertypedef
charexternreturnunion
constfloatshortunsigned
continueforsignedvoid
defaultgotosizeofvolatile
doifstaticwhile

void关键字:空类型的关键字,void关键字不能定义变量,void用来修饰函数的参数或返回值,代表函数没有参数或没有返回值。

sizeof关键字:使用来测变量、数组的占用存储空间的大小(字节数)。

typedef关键字 :重命名相关的关键字。在C语言中,除系统定义的标准类型和用户自定义的结构体、共用体等类型之外,还可以使用类型说明语句typedef定义新的类型来代替已有的类型。typedef语句的一般形式是:

typedef 已定义的类型 新的类型;

例如:

typedef int INTEGER; /*指定用 INTEGER 代表 int 类型*/
typedef float REAL; /*指定用 REAL 代表 float 类型*/

在具有上述typedef语句的程序中,下列语句就是等价的:

int i, j; /*与 INTEGER i,j;*/
float pi; /*与 REAL pi;*/

当然typedef的最常用的作用就是给结构体变量重命名。

#include<stdio.h>
#include<string.h>
typedef struct _INFO
{
    int num;
    char str[256];
}INFO;
int main()
{
    struct _INFO A;
    INFO B; //通过typedef重命名后的名字INFO与struct _INFO完全等价!
    A.num = 2014;
    strcpy(A.str,"Welcome to dotcpp.com");
    B=A;
    printf("This year is %d %s\n",A.num,A.str);
    printf("This year is %d %s\n",B.num,B.str);
    return 0;
}

可以看到typedef可以为关键词改名,使改名之后的INFO类型等价于struct _INFO类型,让我们在定义这种结构类型时更方便、省事。


3. 存储相关的关键字

register、static、const、auto、extern

register 是 寄存器的意思,用register修饰的变量是寄存器变量。即:在编译的时候告诉编译器这个变量是寄存器变量,尽量将其存储空间分配在寄存器中。

注意:
(1):定义的变量不一定真的存放在寄存器中。
(2):cpu 取数据的时候去寄存器中拿数据比去内存中拿数据要快
(3):因为寄存器比较宝贵,所以不能定义寄存器数组
(4):register 只能修饰 字符型及整型的,不能修饰浮点型

 register char ch;
 register short int b;
 register int c;
 register float d;//错误的

(5):因为register 修饰的变量可能存放在寄存器中不存放在内存中,所以不能对寄存器变量取地址。因为只有存放在内存中的数据才有地址。

 register int a;
 int *p;
 p=&a;//错误的,a可能没有地址

static 是静态的意思,static 可以修饰全局变量、局部变量、函数。

const 是常量的意思,用const 修饰的变量是只读的,不能修改它的值。const 可以修饰指针。

const int a=101;//在定义 a 的时候用const 修饰,并赋初值为101

从此以后,就不能再给a赋值了。

extern 是外部的意思,一般用于函数和全局变量的声明。


4. if选择结构

if else选择程序结构用于判断给定的条件,根据判断条件的成立与否来控制程序的流程。选择结构有单选择、双选择和多选择3种形式,单选择结构用if语句实现。

形式一:

if(表达式) /*若条件成立则实行花括号里的语句,反之则不执行*/ 
{ 
    //语句 
}

形式二:

if(表达式) /*若表达式成立则执行语句1,否则执行语句2*/ 
{ 
    //语句1 
} 
else 
{ 
    //语句2 
}

形式三:

if(表达式) /*如果表达式成立,执行语句1否则继续判断表达式2*/ 
{ 
    //语句1 
} 
else if(表达式2) /*如果表达式成立,执行语句2否则继续判断表达式3*/ 
{ 
    //语句2 
} 
else if(表达式3) /*如果表达式成立,则执行语句3否则继续判断下一个表达式*/ 
{ 
    //语句3; 
} 
//… … 
else /*如果以上表达式都不成立 则执行语句4*/ 
{ 
    //语句4 
}

5. switch case语句

对于有三种或更多的结构,C语言除了用多分支选择结构else if之外,C语言还提供了switch的结构。

switch语句的执行过程为:首先计算表达式的值,然后依次与常量表达式依次进行比较,若表达式的值与某常量表达式相等,则从该常量表达式处开始执行,直到switch语句结束。若所有的常量表达式的值均不等于表达式的值,则从default处开始执行。一般形式如下:

switch(表达式) /*首先计算表达式的值*/ 
{ 
    case 常量表达式1:语句1; 
    case 常量表达式2:语句2; 
    case 常量表达式3:语句3; 
    // …… 
    case 常量表达式n:语句n; 
    default:语句n+1;
}

6. break语句的用法

break,顾名思义,跳出的意思,仅用于跳出switch结构或循环结构,用于提前结束switch结构或循环。

如switch结构中,我们知道switch结构会判断从哪个case开始执行,然后接着后面所有的case后面的语句都执行完,但通常情况下我们希望仅执行一个case后面的语句,不希望输出多余的信息,因此这个时候就可以使用break语句跳出结束switch结构,如以下程序:

#include<stdio.h>
int main()
{
    int value;
    scanf("%d",&value);
    switch(value)
    {
        case 1:printf("one");break;
        case 2:printf("two");break;
        case 3:printf("three");break;
        default:printf("other");break;
    }
    return 0;
}

7. while循环语句

while语句创建一个循环,该循环在判断表达式为假(或0)之前重复执行。while语句是一个入口条件(entry-condition)循环,在进行一次循环之前决定是否要执行循环。因此有可能一次也不执行。循环的语句部分可以是一个简单语句或一个复合语句。

while(表达式) 
{ 
    循环体语句 
}

do while语句创建一个循环,它在判断表达式为假(或0)之前重复执行。do while语句是一个退出条件循环,在执行一次循环之后才决定是否要再次执行循环,因此循环至少要被执行一次。循环的语句部分可以是一个简单语句或一个复合语句。

do 
{ 
    循环体语句 
}while(表达式);

8. for循环语句

for语句使用由分号隔开的三个控制表达式来控制循环过程。初始化表达式只在开始执行循环语句之前执行一次。如果判断表达式为真(或非0)就执行一次循环。然后计算更新表达式并再次检查判断表达式的值。for语句是一个入口条件循环,在进行一次循环之前决定是否要执行循环,因此有可能循环一次也不执行。循环的语句部分可以是一个简单语句或一个复合语句。

for(初始化表达式;判断表达式;更新表达式)
{
    循环体语句 
}

案例:

#include<stdio.h>
int main()
{
    int i;
    for(i=0;i<20;i++)
    {
        printf("count is %d\n",i);
    }
    return 0;
}

9. continue语句

continue,顾名思义,是继续的意思,它仅用于循环中,用于提前结束本次循环,即跨过continue后面的循环语句,提前进入下次循环。continue只能在循环中使用!

我们可以写一个循环,从0~100,然后呢做一个if判断,如果发现是奇数就过滤掉,进入下次循环,如果是偶数就加起来。这样循环结束就是需求的结果了。

#include<stdio.h>
int main()
{
    int n=0;
    int sum=0;
    for(n=0;n<100;n++)
    {
        if(n%2!=0) //如果对2取余不等于0,说明没有整除,当然不是偶数啦
        {
            continue;
        }
        sum=sum+n;
    }
    printf("%d\n",sum);
    return 0;
}

10. 格式化输出函数printf

printf函数叫做格式输出函数,其功能是按照用户指定的格式,把指定的数据输出到屏幕上,printf函数的格式为:

printf("格式控制字符串",输出表项);

格式字符串的形式为:% [输出最小宽度] [.精度] [长度] 类型

例如:%d格式符表示用十进制整形格式输出,%5.2f格式表示输出宽度为5(包括小数点),并包含2位小数。

常用的输出格式及含义如下:

格式字符
d , i以十进制形式输出有符号整数(正数不输出符号)
O以八进制形式输出无符号整数(不输出前缀0)
x以十六进制形式输出无符号整数(不输出前缀0x)
U以十进制形式输出无符号整数
f以小数形式输出单、双精度类型实数
e以指数形式输出单、双精度实数
g以%f或%e中较短输出宽度的一种格式输出单、双精度实数
C输出单个字符
S输出字符串

*修饰符在printf()中的用法:

假如您不想事先指定字段宽度,而是希望由程序来制定该值,那么您可以在字段宽度部分使用*代替数字来达到目的,但是您也必须使用一个参数来告诉函数宽度的值是多少。具体的说,如果转换说明符为%d,那么参数列表中应该包括一个的值和一个d的值,来控制宽度和变量的值。该技术也可以和浮点值一起使用来指定精度和字段宽度。

#include<stdio.h>
int main(void)
{
    unsigned width,precision;
    int number = 256;
    double weight = 25.5;
    printf("Please input number's width:\n");
    scanf("%d",&width);
    printf("The number is: %*d\n",width,number);
    printf("Then please input width and precision:\n");
    scanf("%d %d",&width,&precision);
    printf("Weight = %*.*f\n",width,precision,weight);
    return 0;
}

11. 类型转换

数据有不同的类型,不同类型数据之间进行混合运算时必然涉及到类型的转换问题.

自动转换的原则:占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低.

强制转换:通过类型转换运算来实现。(类型说明符)(表达式)

(float)a; // 把 a 的值转换为实型
(int)(x+y); // 把 x+y 的结果值转换为整型


三. 数组

数组是若干个相同类型的变量在内存中有序存储的集合。

1. 数组的分类

按元素的类型分类:
1)字符数组
即若干个字符变量的集合,数组中的每个元素都是字符型的变量
char s[10];

2)短整型的数组
short int a[10];

3)整型的数组
int a[10];

4)长整型的数组
lont int a[5];

5)浮点型的数组(单、双)
float a[6]; a[4]=3.14f;
double a[8]; a[7]=3.115926;

6)指针数组
char *a[10]
int *a[10];

7)结构体数组
struct stu boy[10];

按维数分类
分为一维数组和多维数组。

一维数组的定义和使用:在C语言中使用数组必须先进行定义,一维数组的定义方式:数据类型 数组名 [数组元素个数];

二维数组的定义何使用: 数据类型 数组名 [行的个数][列的个数];

多维数组定义: int a[3][4][5]

二维数组在定义的时候,可以不给出行数,但必须给出列数,二维数组的大小根据初始化的行数来定:

#include<stdio.h>
 intmain(intargc,char*argv[])
 {
 inta[][3]={
 {1,2,3},
 {4,5,6},
 {7,8,9},
 {10,11,12}
 };
 printf("%d\n",sizeof(a));
 return0;
 }

2. 数组的初始化

定义数组的时候,顺便给数组的元素赋初值,即开辟空间的同时并且给数组元素赋值。

一维数组的初始化

(1)全部初始化

inta[5]={2,4,7,8,5};
代表的意思: a[0]=2;a[1]=4;a[2]=7;a[3]=8;a[4]=5;

(2) 部分初始化

int a[5]={2,4,3};初始化赋值不够后面补0
a[0]=2;a[1]=4;a[2]=3;a[3]=0;a[4]=0

二维数组的定义并初始化

(1)按行初始化:

a、全部初始化
int a[2][2]={{1,2},{4,5}};
a[0][0] =1; a[0][1] = 2; a[1][0] = 4,a[1][1]=5;

b、部分初始化
int a[3][3]={{1,2},{1}};
a[0][0] = 1;a[0][2] =0;

(1)逐个初始化:

a、全部初始化:
int a [2][3]={2,5,4,2,3,4};

b、部分初始化:
int a[2][3]={3,5,6,8}


四. 函数

1. 函数的分类

(1) 从定义角度分类(即函数是谁实现的)

库函数 (c库实现的)
自定义函数 (程序员自己实现的函数)
系统调用 (操作系统实现的函数)

(2) 从参数角度分类

有参函数 : 函数有形参,可以是一个,或者多个,参数的类型随便完全取决于函数的功能。

int fun(int a,float b,double c)
 {
 }
 
 int max(int x,int y)
 {
 }

无参函数 : 函数没有参数,在形参列表的位置写个void或什么都不写。

int fun(void)
 {
 }

int fun()
{
}

(3) 从返回值角度分类

带返回值的函数 : 在定义函数的时候,必须带着返回值类型,在函数体里,必须有return。如果没有返回值类型,默认返回整型。

//定义了一个返回字符数据的函数
char fun()
{
 char b='a';
 return b;
 }

// 如果把函数的返回值类型省略了,默认返回整型
fun()
 {
 return 1;
 }

没返回值的函数 : 在定义函数的时候,函数名字前面加void

 void fun(形参表)
 {
 }

2. 函数的定义

函数的定义通常包含以下内容:

返回值类型 函数名(形参表说明) /*函数首部*/
{
    说明语句 /*函数体*/
    执行语句
}

例子:

 int max(int x, int y)
 {
 int z;
 if(x > y){
 	z = x;
 }else{
 	z = y;
 }
 return z;
 }

3. 函数的声明

对已经定义的函数,进行说明,函数的声明可以声明多次。

有些情况下,如果不对函数进行声明,编译器在编译的时候,可能不认识这个函数,因为编译器在编译c程序的时候,从上往下编译的。

主调函数和被调函数在同一个.c文件中,被调函数在上,主调函数在下的时候就需要声明函数。如下:

#include <stdio.h>

void test1();

int main()
{
	test1();
	return 0;
}

void test1() {
	printf("test1");
}

主调函数和被调函数不在同一个.c文件中的时候,将函数的声明放在头文件中,.c程序要调用,包含头文件即可。

test.c

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void test1() {
	printf("test1");
}

test.h

extern void test1();

main.c

#include<stdio.h>
#include "test.h"

int main()
{
	test1();
	return 0;
}

4. 变量的存储类别

内存的分区:

物理内存:实实在在存在的存储设备

虚拟内存:操作系统虚拟出来的内存。

操作系统会在物理内存和虚拟内存之间做映射。在32位系统下,每个进程的寻址范围是4G,0x00000000 ~0xffffffff。在写应用程序的,咱们看到的都是虚拟地址。在运行程序的时候,操作系统会将 虚拟内存进行分区。


:在动态申请内存的时候,在堆里开辟内存。

:主要存放局部变量。

静态全局区:(1)未初始化的静态全局区:静态变量(定义变量的时候,前面加static修饰),或全局变量 ,没有初始化的,存在此区。(2)初始化的静态全局区:全局变量、静态变量,赋过初值的,存放在此区

代码区:存放咱们的程序代码

文字常量区:存放常量的。


(1)普通的全局变量

概念:在函数外部定义的变量.

//num 就是一个全局变量
int num=100;
int main()
 {
 return 0;
 }

作用范围:普通全局变量的作用范围,是程序的所有地方。只不过用之前需要声明。声明方法 extern int num;。

(2) 静态全局变量 static

概念:定义全局变量的时候,前面用static 修饰。

//num 就是一个静态全局变量
static int num=100;
int main()
 {
 }

作用范围:static 限定了静态全局变量的,作用范围。**只能在它定义的.c(源文件)中有效。**在程序的整个运行过程中,一直存在。

(3)普通的局部变量

概念:在函数内部定义的,或者复合语句中定义的变量。

int main()
{

 int num; //普通局部变量
 
 {
 int a; //普通局部变量
 }
 
}

作用范围:在函数中定义的变量,在它的函数中有效。在复合语句中定义的,在它的复合语句中有效。在函数调用之前,局部变量不占用空间,调用函数的时候,才为局部变量开辟空间,函数结束了,局部变量就释放了

#include<stdio.h>

void fun() {
	int num = 3;
	num++;
	printf("num=%d\n", num);
}


int main()
{
	fun(); // num=4
	fun(); // num=4
	fun(); // num=4
	return 0;
}

(4)静态的局部变量

概念:定义局部变量的时候,前面加static修饰。

作用范围:在它定义的函数或复合语句中有效。第一次调用函数的时候,开辟空间赋值,函数结束后,不释放,以后再调用函数的时候,就不再为其开辟空间,也不赋初值,用的是以前的那个变量。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void fun() {
	static int num = 3;
	num++;
	printf("num=%d\n", num);
}


int main()
{
	fun(); // num=4
	fun(); // num=5
	fun(); // num=6
	return 0;
}

变量存储类别扩展:在同一作用范围内,不允许变量重名。作用范围不同的可以重名。局部范围内,重名的全局变量不起作用。(就近原则)

外部函数:咱们定义的普通函数,都是外部函数。即函数可以在程序的任何一个文件中调用。

内部函数:在定义函数的时候,返回值类型前面加static 修饰。这样的函数被称为内部函数。static 限定了函数的作用范围,在定义的.c中有效。


五. 预处理

预处理命令可以改变程序设计环境,提高编程效率,它们并不是C语言本身的组成部分,不能直接对它们进行编译,必须在对程序进行编译之前,先对程序中这些特殊的命令进行“预处理”。经过预处理后,程序就不再包括预处理命令了,最后再由编译程序对预处理之后的源程序进行编译处理,得到可供执行的目标代码。C语言提供的预处理功能有三种,分别为宏定义、文件包含和条件编译,下面将对它们进行简单介绍。

1. C语言编译过程

1):预编译
将.c 中的头文件展开、宏展开,生成的文件是.i文件。

 gcc -E hello.c -o hello.i

2):编译
将预处理之后的.i 文件生成 .s 汇编文件

 gcc -S hello.i –o hello.s

3)、汇编
将.s 汇编文件生成.o 目标文件

 gcc -c hello.s -o hello.o

4)、链接
将.o 文件链接成目标文件

 gcc hello.o -o hello_elf

2. 文件包含 #include

文件包含是C预处理程序的另一个重要功能,文件包含命令行的一般形式为:

#include <文件名> :用尖括号包含头文件,在系统指定的路径下找头文件。

#include "文件名" :用双引号包含头文件,先在当前目录下找头文件,找不到,再到系统指定的路径下找。

注意:include 经常用来包含头文件,可以包含 .c 文件,但是大家不要包含.c。因为include 包含的文件会在预编译被展开,如果一个.c 被包含多次,展开多次,会导致函数重复定义。预处理只是对include 等预处理操作进行处理并不会进行语法检查,这个阶段有语法错误也不会报错,第二个阶段即编译阶段才进行语法检查。

一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。


3. 宏定义 #define

宏定义在C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”,被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。在C语言中,宏分为有参数和无参数两种。

无参宏的宏名后不带参数,其定义的一般形式为:#define 标识符 字符串; ,“字符串”可以是常数、表达式、格式串等。

常常对程序中反复使用的表达式进行宏定义。例如:

#define M (y*y+3*y);

它的作用是指定标识符M来代替表达式(yy+3y)。在编写源程序时,所有的(yy+3y)都可由M代替,而对源程序进行编译时,将先由预处理程序进行宏代换,即用(yy+3y)表达式去置换所有的宏名M,然后再进行编译。

C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对于带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。

带参宏定义的一般形式为:#define 宏名(形参表) 字符串;

带参宏调用的一般形式为:宏名(实参表);

举例:

#include <stdio.h>
#define MAX(a,b) (a>b)?a:b
/*带参数的宏定义*/
main()
{
    int x,y,max;
    printf("input two numbers: ");
    scanf("%d %d",&x,&y);
    max=MAX(x,y);
    printf("max=%d\n",max);
    /*宏调用*/
}</stdio.h>

结果如下:

input two numbers: 2009 2010↙
max=2010

可以看到,宏替换相当于实现了一个函数调用的功能,而事实上,与函数调用相比,宏调用更能提高C程序的执行效率。

#define 后面只有一个参数的语法:
一般情况下,宏定义时的用法为:#define a b ,后接两个参数,表示用a代替b。很多时候,#define 后只有一个参数,经常出现在头文件的开始处。

用法解释: 定义宏,并在预处理过程中将其替换为空字符串(即删除)。这样做主要是为了标记某些内容,使程序阅读者能够清楚标记表明的意义,同时又不影响被编译的源代码。也就是说,用法同define后接两个参数一样,只是后一个参数为空字符串。用途包括:(1)定义一个符号用来给#if(n)def判断。(2)多文件编译中防止头文件被重复包含。


4. 条件编译

预处理程序提供了条件编译的功能,可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件,这对于程序的移植和调试是很有用的。条件编译可分为三种形式。

(1)第一种形式如下:

#ifdef 标识符
程序段 1
#else
程序段 2
#endif

它的功能是如果 标识符 已被 #define 命令定义过则对 程序段1 进行编译;否则对 程序段2 进行编译。

如果没有程序段2(为空),本格式中的#else可以没有,即可以写为:

#ifdef 标识符
程序段
#endif

(2)第二种形式如下:

#ifndef 标识符
程序段 1
#else
程序段 2
#endif

与第一种形式的区别是将 “ifdef” 改为 “ifndef” 。它的功能是如果 标识符 未被 #define 命令定义过则对 程序段1 进行编译,否则对 程序段2 进行编译。这与第一种形式的功能正好相反。

(3)第三种形式如下:

#if 常量表达式
程序段 1
#else
程序段 2
#endif

它的功能是如果常量表达式的值为真(非0),则对程序段1进行编译,否则对程序段2进行编译。因此可以使程序在不同的条件下完成不同的功能。


六. 指针

1. 指针的概念

系统给虚拟内存的每个存储单元分配了一个编号,从0x00000000 ~ 0xffffffff ,这个编号咱们称之为地址,指针就是地址。

在这里插入图片描述
指针变量:是个变量,即这个变量用来存放一个地址编号。

在32位平台下,地址总线是32位的,所以地址是32位编号,所以指针变量是32位的即4个字节。

注意:
(1) 无论什么类型的地址,都是存储单元的编号,在32位平台下都是4个字节,在64位系统下,所有类型的指针都是8个字节。

(2) 对应类型的指针变量,只能存放对应类型的变量的地址。举例:整型的指针变量,只能存放整型变量的地址。

扩展:
字符变量 char ch = ‘b’; ch占1个字节,它有一个地址编号,这个地址编号就是ch的地址。
整型变量 int a = 0x12345678; a占4个字节,它占有4个字节的存储单元,有4个地址编号。
在这里插入图片描述


2. 指针变量的定义

(1) 简单的指针变量 : 数据类型 * 指针变量名;

 int * p; //定义了一个指针变量p

在定义指针变量的时候 * 是用来修饰变量的,说明变量p是个指针变量,变量名是 p。

(2) 关于指针的运算符
& : 取地址 、 用* 的指针变量来取值。

int a = 0x1234abcd;
int *p;  //在定义指针变量的时候*代表修饰的意思,修饰p是个指针变量。
p = &a;  //把 a 的地址给p赋值 ,&是取地址符,

p 保存了a的地址,也可以说p指向了a。

扩展:如果在一行中定义多个指针变量,每个指针变量前面都需要加*来修饰。

int *p,*q; //定义了两个整型的指针变量p和q
int * p,q; //定义了一个整型指针变量p,和整型的变量q

举例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	int a = 100, b = 200;
	//指针变量名是p_1而不是 *p_1 ,并且 p_1 在定义的时候没有赋初值,p_2赋了初值 &b
	int *p_1, * p_2 = &b;
	// p_1 可以先定义后赋值
	p_1 = &a;
	printf("%d\n", a); // 100
	printf("%d\n", *p_1); // 100
	printf("%d\n", b); // 200
	printf("%d\n", *p_2); // 200
	return 0;
}

(3) 指针大小
在64位系统下,所有类型的指针都是8个字节。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	char* p1;
	short* p2;
	int* p3;
	long* p4;
	float* p5;
	double* p6;

	printf("%d\n", sizeof(p1)); //8
	printf("%d\n", sizeof(p1)); //8
	printf("%d\n", sizeof(p3)); //8
	printf("%d\n", sizeof(p4)); //8
	printf("%d\n", sizeof(p5)); //8
	printf("%d\n", sizeof(p6)); //8

	return 0;
}

3. 指针和变量的关系

指针可以存放变量的地址编号

 int a=100;
 int *p;
 p=&a;

在程序中,引用变量的方法 :

直接通过变量的名称 :

 int a;
 a=100;

还可以通过指针变量来引用变量:

int *p; //在定义的时候,*不是取值的意思,而是修饰的意思,修饰p是个指针变量
p=&a; //取 a 的地址给p赋值,p保存了a的地址,也可以说p指向了a
*p= 100; //在调用的时候*是取值的意思,*指针变量 等价于指针指向的变量

注意:
1:*指针 取值,取几个字节,由指针类型决定的指针为字符指针则取一个字节,指针为整型指针则取4个字节,指针为double型指针则取8个字节。
2:指针++ 指向下个对应类型的数据。字符指针++ ,指向下个字符数据,指针存放的地址编号加1,整型指针++ 指向下个整型数据,指针存放的地址编号加4。


4. 指针和数组元素之间的关系

变量存放在内存中,有地址编号,咱们定义的数组,是多个相同类型的变量的集合,每个变量都占内存空间,都有地址编号,指针变量当然可以存放数组元素的地址。

int main()
{
	int a[5];
	// 指针变量p保存了数组a中第0个元素的地址,即a[0]的地址
	int* p = &a[0];
	return 0;
}

在这里插入图片描述

数组元素的引用方法:

(1) 方法1: 数组名[下标] :

 int a[5];
 a[2]=100;

(2) 方法2:指针名加下标 :

 int a[5];
 int *p;
 p=a;
 p[2]=100; //相当于 a[2]=100;

补充:c语言规定:数组的名字就是数组的首地址,即第0个元素的地址,就是&a[0] 。

注意:p和a的不同,p是指针变量,而a是个常量。所以可以用等号给p赋值,但不能给a赋值。

p=&a[3]; //正确
a=&a[3]; //错误

(3) 通过指针变量运算加取值的方法来引用数组的元素

 int a[5];
 int *p;
 p=a;
 *(p+2)=100; //也是可以的,相当于a[2]=100

解释:p是第0个元素的地址,p+2是a[2]这个元素的地址。

(4) 方法4:通过数组名+取值的方法引用数组的元素

 int a[5];
 *(a+2)=100;//也是可以的,相当于a[2]=100;

注意:a+2是a[2]的地址。这个地方并没有给a赋值。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	int a[5] = { 0,1,2,3,4 };
	int* p;
	p = a;
	printf("a[2]=%d\n", a[2]); // a[2]=2
	printf("p[2]=%d\n", p[2]); // p[2]=2
	printf("*(p+2)=%d\n", *(p + 2)); // *(p+2)=2
	printf("*(a+2)=%d\n", *(a + 2)); // *(a+2)=2
	printf("p=%p\n", p); // p=00000011F94FFC78
	printf("p+2=%p\n", p + 2); // p+2=00000011F94FFC80
	return 0;
}

5. 指针的运算

(1) 指针可以加一个整数,往下指几个它指向的变量,结果还是地址。
前提:指针指向数组元素的时候,加一个整数才有意义

 int a[5];
 int *p;
 p=a;
 p+2;//p是a[0]的地址,p+2是&a[2]
 char buf[5]char *q;
 q=buf;
 q+2 //相当于&buf[2]

(2) 两个相同类型指针可以比较大小。
前提:只有两个相同类型的指针指向同一个数组里面的元素的时候,比较大小才有意义。指向前面元素的指针小于指向后面元素的指针。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	int a[10];
	int* p, * q; // 如果在一行上定义多个指针变量的,每个变量名前面加*
	p = &a[1];
	q = &a[6];
	// p<q
	printf("p=%d\n", p); // p=-1961626100
	printf("q=%d\n", q); // q=-1961626080
	return 0;
}

(3) 两个相同类型的指针可以做减法。
前提:必须是两个相同类型的指针指向同一个数组的元素的时候,做减法才有意义。做减法的结果是,两个指针指向的中间有多少个元素。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	int a[5];
	int* p, * q;
	p = &a[0];
	q = &a[3];
	printf("%d\n", q - p); // 3
	return 0;
}

结果是3。

(4) 两个相同类型的指针可以相互赋值。
前提 : 只有相同类型的指针才可以相互赋值(void*类型的除外)。

 int *p;
 int *q;
 int a;
 p=&a;  //p 保存 a 的地址,p指向了变量a
 q=p;   //用 p 给q 赋值,q也保存了a的地址,指向a

注意:如果类型不相同的指针要想相互赋值,必须进行强制类型转换。


6. 指针数组

(1) 指针和数组的关系

可以定义一个数组,数组中有若干个相同类型指针变量,这个数组被称为指针数组,如 int*p[5]

指针数组的概念:指针数组本身是个数组,是个指针数组,是若干个相同类型的指针变量构成的集合。

(2) 指针数组的定义方法

类型说明符 * 数组名 [元素个数];

举例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	char *name[5] = { "hello","China","beijing","project","Computer" };
	int i;
	for (i = 0; i < 5; i++) {
		printf("%s\n", name[i]);
	}
	return 0;
}

输出:

hello
China
beijing
project
Computer

在这里插入图片描述
“hello”、“China”“beijing” “project” “computer” 这 5 个字符串存放在文字常量区。

假设:
“hello ”首地址是 0x00002000
“China”首地址是 0x00003000
“beijing”首地址是 0x00004000
“project”首地址是 0x00005000
“Computer”首地址是 0x00006000

则:
name[0]中存放内容为 0x00002000
name[1]中存放内容为 0x00003000
name[2]中存放内容为 0x00004000
name[3]中存放内容为 0x00005000
name[4]中存放内容为 0x00006000

在这里插入图片描述
注意:name[0]、name[1]、 name[2]、 name[3]、 name[4] 分别是 char * 类型的指针变量,分别存放一个地址编号。


7. 指针的指针

指针的指针,即指针的地址,咱们定义一个指针变量本身指针变量占4个字节,指针变量也有地址编号。

例:

int a=0x12345678int *p;
 p =&a;

假如:a的地址是 0x00002000。则 p中存放的是a的地址编号即 0x00002000

因为p也占4个自己内存,也有它自己的地址编号,及指针变量的地址,即指针的指针。

假如:指针变量p的地址编号是0x00003000,这个地址编号就是指针的地址。我们定义一个变量存放p的地址编号,这个变量就是指针的指针。

 int **q;
 q=&p;  //q 保存了p 的地址,也可以说q指向了p

则q里存放的就是0x00003000

还可以无限套娃:

int ***m;
m=&q;

在这里插入图片描述


8. 字符串和指针

字符串的概念:字符串就是以’\0’结尾的若干的字符的集合:比如“helloworld”。

字符串的地址,是第一个字符的地址。如:字符串“helloworld”的地址,其实是字符串中字符’h’的地址。

我们可以定义一个字符指针变量保存字符串的地址,比如:

char *s=”helloworld”;

字符串的存储形式: 数组、文字常量区、堆

(1) 字符串存放在数组中
其实就是在内存(栈、静态全局区)中开辟了一段空间存放字符串。

char str[100] = “I love C!

定义了一个字符数组str,用来存放多个字符,并且用”I love C!”给str数组初始化 ,字符串“I love C!”存放在str中。

注:
普通全局数组,内存分配在静态全局区。
普通局部数组,内存分配在栈区。
静态数组(静态全局数组、静态局部数组),内存分配在静态全局区

(2) 字符串存放在文字常量区
在文字常量区开辟了一段空间存放字符串,将字符串的首地址给指针变量。

char *str = “I love C!

定义了一个指针变量str , 只能存放字符地址编号,I love C! 这个字符串中的字符不是都存放在str指针变量中。str 只是存放了字符I的地址编号,“IloveC!”存放在文字常量区。

(3) 字符串存放在堆区
使用malloc 等函数在堆区申请空间,将字符串拷贝到堆区。

char *str =(char*)malloc(10);  //动态申请了 10 个字节的存储空间,
strcpy(str,"I love C")//首地址给str赋值,字符串“Ilove C!”拷贝到 str 指向的内存里。

字符串的可修改性:字符串内容是否可以修改,取决于字符串存放在哪里。

(1)存放在数组中的字符串的内容是可修改的(注:数组没有用const修饰)

 char str[100]=”I love C!;
 str[0]=‘y’;  //正确可以修改的

(2)文字常量区里的内容是不可修改的

 char *str=”I love C!;
 *str =’y’;  //错误,I 存放在文字常量区,不可修改

注:str 指向文字常量区的时候,它指向的内存的内容不可被修改。

(3) 堆区的内容是可以修改的

 char *str =(char*)malloc(10);
 strcpy(str,"I love C");
 *str=’y’;  //正确,可以,因为堆区内容是可修改的

注:
1、str 指向堆区的时候,str指向的内存内容是可以被修改的。
2、str 是指针变量,也可以指向别的地方。即可以给str重新赋值,让它指向别的地方

字符数组,可以使用scanf或者strcpy函数赋值:

char buf[20]=”hello world”
buf="hello kitty"; //错误,因为字符数组的名字是个常量,不能用等号给常量赋值。
strcpy(buf,"hello kitty");  //正确,数组中的内容是可以修改的
scanf("%s",buf);  //正确,数组中的内容是可以修改的

指针指向文字常量区时赋值:

char *buf_point = “hello world”;
buf_point="hello kitty";  // 正确,buf_point 指向另一个字符串
strcpy(buf_point,"hello kitty");  // 错误,这种情况,buf_point 指向的是文字常量区,内容只读。

指针指向堆区,堆区存放字符串时的赋值:

 char *buf_heap;
 buf_heap=(char *)malloc(15);
 strcpy(buf_heap,"hello world");
 scanf(%s”,buf_heap);

9. 数组指针

回顾:数组的名字是数组的首地址,是第0个元素的地址,是个常量,数组名字加1指向下个元素。

二维数组a中,a+1指向下个元素,即下一个一维数组,即下一行。

int main()
{
	int a[3][5];
	printf("a=%p\n", a); 
	printf("a+1=%p\n", a + 1); 
	return 0;
}

数组指针的概念:本身是个指针,指向一个数组,加1跳一个数组,即指向下个数组。

数组指针的定义方法:指向的数组的类型(*指针变量名)[指向的数组的元素个数]

// 定义了一个数组指针变量p,p指向的是整型的有5个元素的数组
 int (*p)[5];

例1:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	// 定义了一个3行5列的一个二维数组
	int a[3][5];
	// 定义一个数组指针变量p
	int(*p)[5];

	printf("a=%p\n", a); // 第0行的行地址
	printf("a+1=%p\n", a + 1); // 第1行的行地址,a和a+1差20个字节
	p = a;
	printf("p=%p\n", p); // 第0行的行地址
	printf("p+1=%p\n", p + 1);//第1行的行地址
	return 0;
}

数组指针的用法的举例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void fun(int(*p)[5], int x, int y) {
	p[0][1] = 101;
}


int main()
{
	int i, j;
	int a[3][5];
	fun(a, 3, 5);
	for (i = 0; i < 3; i++) {
		for (j = 0; j < 5; j++) {
			printf("%d ", a[i][j]);
		}
		printf("\n");
	}
	return 0;
}

10. 各种数组指针的定义

(1)、一维数组指针,加1后指向下一行的一维数组。用法和上一节的用法一样。

如 int (*p)[5]; ,配合每行有5个int型元素的二维数组如 int a[3][5] 、int b[4][5]、 int c[5][5]、int d[6][5]、 ……,使用 p=a;、、 p=b;、 p=c; 或者 p=d; 都是可以的。

(2)、二维数组指针,加1后指向下个二维数组
如 int (*p)[4][5]; 配合三维数组来用。三维数组中由若干个4行5列二维数组构成,如int a[3][4][5];、 int b[4][4][5];、int c[5][4][5];、 int d[6][4][5]; 这些三维数组,有个共同的特点,都是有若干个4行5的二维数组构成。可以使用 p=a;、 p=b;、 p=c;或 p=d; 都是可以的。

举例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	int a[3][4][5];
	printf("a=%p\n", a);
	printf("a+1=%p\n", a + 1);//a 和 a+1 地址编号相差 80 个字节
	// 验证了a+1 跳一个4行5列的一个二维数组

	int(*p)[4][5];
	p = a;
	printf("p=%p\n", p);
	printf("p+1=%p\n", p + 1);//p 和 p+1 地址编号相差也 80 个字节
	return 0;
}

以此类推,还有三维数组指针、四维数组指针、。。。。


11. 数组名字取地址:变成数组指针

一维数组名字取地址,变成一维数组指针,即加1跳一个一维数组。

int a[10];

a+1 跳一个整型元素,是a[1]的地址。a 和a+1 相差一个元素,4个字节

&a 就变成了一个一维数组指针,是 int(*p)[10]类型的。(&a) +1 和 &a 相差一个数组即10个元素即40个字节。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	int a[10];
	printf("a=%p\n", a);
	printf("a+1=%p\n", a + 1);
	printf("&a=%p\n", &a);
	printf("&a +1=%p\n", &a + 1);
	return 0;
}

运行结果:
在这里插入图片描述
在运行程序时,大家会发现a和&a所代表的地址编号是一样的,即他们指向同一个存储单元,但是a和&a的指针类型不同。


12. 数组名字和指针变量的区别

 int a[5];
 int *p;
 p=a;

相同点:

a 是数组的名字,是a[0]的地址,p=a即p保存了a[0]的地址,即a和p都指向a[0],所以在引用数组元素的时候,a和p等价。

引用数组元素回顾:a[2]、(a+2)、p[2]、(p+2) 都是对数组 a 中a[2]元素的引用。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	int a[5] = { 0,1,2,3,4 };
	int* p;
	p = a;
	printf("a[2] = %d\n", a[2]);
	printf("*(a + 2) = % d\n", *(a + 2));
	printf("p[2] = %d\n", p[2]);
	printf("*(p + 2) = % d\n", *(p + 2));
	return 0;
}

运行结果:
在这里插入图片描述

不同点:

1、a是常量、p是变量。可以用等号 = 给p赋值,但是不能用等号给a赋值。
2、对a取地址,和对p取地址结果不同。因为a是数组的名字,所以对a取地址结果为数组指针。p 是个指针变量,所以对p取地址(&p)结果为指针的指针。


13. 数组指针取 *

数组指针取 *,并不是取值的意思,而是指针的类型发生变化:

一维数组指针取*,结果为它指向的一维数组第0个元素的地址,它们还是指向同一个地方。

二维数组指针取*,结果为一维数组指针,它们还是指向同一个地方。

三维数组指针取*,结果为二维数组指针,它们还是指向同一个地方。

多维以此类推。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main()
{
	int a[3][5];
	int(*p)[5];
	p = a;
	printf("a=%p\n", a); //a是一维数组指针,指向第0个一维数组,即第0行
	printf("*a=%p\n", *a); /*a是第0行第0个元素的地址,即&a[0][0]
	printf("*a+1=%p\n", *a+1); //*a+1是第0行第1个元的地址,即&a[0][1]
	printf("p=%p\n", p); //p是一维数组指针,指向第0个一维数组,即第0行
	printf("*p=%p\n", *p); //*p是第0行第0个元素的地址,即&a[0][0]
	printf("*p+1=%p\n", *p + 1); //*p+1是第0行第1个元的地址,即&a[0][1]
	return 0;
}

14. 指针作为函数的参数

咱们可以给一个函数传一个整型、字符型、浮点型的数据,也可以给函数传一个地址。

例1:

int num;
 scanf("%d",&num);

例2:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void swap(int* p1, int* p2) {
	int temp;
	temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}

int main()
{
	int a = 10, b = 20;
	swap(&a, &b);
	printf("a=%d b=%d\n", a, b);//结果为a=20 b=10
	return 0;
}

结论:调用函数的时候传变量的地址,在被调函数中通过*+地址来改变主调函数中的变量的值。

例3;

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

//此函数中改变的是p1和p2的指向,并没有给main中的a和b赋值
void swap(int* p1, int* p2) {
	int* p;
	p = p1;
	p1 = p2;//p1=&b,让p1指向main中的b
	p2 = p;//p2=&a,让p2指向main函数中a
}

int main()
{
	int a = 10, b = 20;
	swap(&a, &b);
	printf("a=%d b=%d\n", a, b);//结果为a=10 b=20
	return 0;
}

总结:要想改变主调函数中变量的值,必须传变量的地址,而且还得通过 * + 地址去赋值。

例4:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void fun(char* p) {
	p = "hello kitty";
}


int main()
{
	char* p = "hello world";
	fun(p);
	printf("%s\n", p);//结果为:hello world
	return 0;
}

在fun中改变的是fun函数中的局部变量p,并没有改变main函数中的变量p,所以main函数中的,变量p还是指向helloworld。

例5:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void fun(char** p) {
	*p = "hello kitty";
}


int main()
{
	char* p = "hello world";
	fun(&p);
	printf("%s\n", p);//结果为:hello kitty
	return 0;
}

总结一句话:要想改变主调函数中变量的值,必须传变量的地址,而且还得通过*+地址去赋值。无论这个变量是什么类型的。


15. 给函数传数组

给函数传数组的时候,没法一下将数组的内容作为整体传进去。只能传数组名进去,数组名就是数组的首地址,即只能把数组的地址传进去。也就是说,只能传一个4个字节大小的地址编号进去

例1:传一维数组的地址

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void fun(int *p) {
	printf("%d\n", p[2]); // 3
	printf("%d\n", *(p + 3)); // 4
}


int main()
{
	int a[10] = { 1,2,3,4,5,6,7,8 };
	fun(a);
	return 0;
}

例2:传二维数组的地址

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void fun(int(*p)[4]) {

}


int main()
{
	int a[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
	fun(a);
	return 0;
}

例3:传指针数组

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void fun(char** q) {
	int i;
	for (i = 0; i < 3; i++) {
		printf("%s ", q[i]);
	}
}


int main()
{
	char* p[3] = { "hello","world","kitty" };
	fun(p);
	return 0;
}

//打印 hello world kitty


16. 指针作为函数的返回值

一个函数可以返回整型数据、字符数据、浮点型的数据,也可以返回一个指针。

例1:返回静态局部数组的地址

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

char* fun() {
	char str[100] = "hello world";
	return str;
}


int main()
{
	char* p;
	p = fun();
	printf("%s\n", p); // 乱码
	return 0;
}

这里返回的是fun函数里面局部变量str的地址,函数运行结束,内存就被释放了,返回这个地址,也没有意义了。

更改一下,在函数里面给局部变量加static修饰,原因是,静态数组的内容,在函数结束后,亦然存在。:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

char* fun() {
	static char str[100] = "hello world";
	return str;
}


int main()
{
	char* p;
	p = fun();
	printf("%s\n", p); // hello world
	return 0;
}

例2:返回文字常量区的字符串的地址

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

char* fun() {
	char* str = "hello world";
	return str;
}


int main()
{
	char* p;
	p = fun();
	printf("%s\n", p); // hello world
	return 0;
}

这里也能行,原因是文字常量区的内容,一直存在。

例3:返回堆内存的地址

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

char* fun() {
	char* str;
	str = (char*)malloc(100);
	strcpy(str, "hello world");
	return str;
}


int main()
{
	char* p;
	p = fun();
	printf("%s\n", p); // hello world
	free(p);
	return 0;
}

原因是堆区的内容一直存在,直到free才释放。

总结:返回的地址,地址指向的内存的内容得存在,返回的地址才有意义。


17. 指针保存函数的地址(函数指针)

(1) 函数指针的概念
咱们定义的函数,在运行程序的时候,会将函数的指令加载到内存的代码段。所以函数也有起始地址。c 语言规定:函数的名字就是函数的首地址,即函数的入口地址。咱们就可以定义一个指针变量,来存放函数的地址。这个指针变量就是函数指针变量。

(2) 函数指针的用处
函数指针用来保存函数的入口地址。在项目开发中,我们经常需要编写或者调用带函数指针参数的函数。比如Linux系统中创建多线程的函数,它有个参数就是函数指针,接收线程函数的入口地址,即创建线程成功后,新的任务执行线程函数。用于给一个函数传另一个函数进去的情况。

(3) 函数指针变量的定义

返回值类型(*函数指针变量名)(形参列表);

例子:

int fun(int x, int y) {
	return 1;
}

// 定义了一个函数指针变量p, p 指向的函数。必须有一个整型的返回值,有两个整型参数。
int(*p)(int, int);
// 可以用这个p存放这类函数的地址。
p = fun;

(4) 调用函数的方法

1.通过函数的名字去调函数(最常用的)

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int fun1(int x, int y) {
	return x+y;
}

int main()
{
	int a=fun1(1,2);
	printf("%d", a); // 3
	return 0;
}

2.可以通过函数指针变量去调用

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int fun2(int x, int y) {
	return x+y;
}

int main()
{
	int (*p)(int, int);
	p = fun2;
	int num = (*p)(1, 2);
	printf("%d", num); // 3
	return 0;
}

18. 函数指针数组

概念:由若干个相同类型的函数指针变量构成的集合,在内存中连续的顺序存储。函数指针数组是个数组,它的每个元素都是一个函数指针变量。

函数指针数组的定义:

类型名(*数组名[元素个数])(形参列表)

例如:

// 定义了一个函数指针数组,数组名是p,有5个元素p[0] ~p[4],每个元素都是函数指针变量,
// 每个函数指针变量指向的函数,必须有整型的返回值,两个整型参数。
int(*p[5])(int,int);

用法举例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int max(int x, int y)
{
	int temp;
	if (x > y)
		temp = x;
	else
		temp = y;
		return temp;
}

int min(int x, int y)
{
	int temp;
	if (x < y)
		temp = x;
	else
		temp = y;
	return temp;
}

int add(int x, int y)
{
	return x + y;
}

int main()
{
	int(*p[3])(int, int) = { max,min,add};
	int num;
	num = (*p[1])(10, 20);
	printf("num=%d\n", num);// num=10
	return 0;
}

19. 特殊指针

(1) 空类型的指针 void *
void * 难道是指向void型的数据吗?不是,因为没有void类型的变量,void* 通用指针,任何类型的地址都可以给void*类型的指针变量赋值。

int *p;
void *q;
q=p // 是可以的,不用强制类型转换

举例:有个函数叫memset, void * memset(void *s,int c,size_t n); ,这个函数的功能是将s指向的内存前n个字节,全部赋值为 c。它的返回值是s指向的内存的首地址,可能是不同类型的地址。所以返回值也得是通用指针。

注意:void*类型的指针变量,也是个指针变量,在32为系统下,占4个字节,64位操作系统,占8个字节。

(2) NULL
空指针

char *p=NULL;

咱们可以认为p哪里都不指向,也可以认为p指向内存编号为0的存储单位。一般NULL用在给指针变量初始化

20. main 函数传参

int argc:argc 表示命令行参数的个数(argument count),包括程序本身。即 argc 的值至少为 1。

char* argv[]:第二个参数argv,可以使用argument value来记忆,参数值的意思。argv[] 是一个指向字符串数组的指针,其中每个元素是一个指向传递给程序的参数的指针(argument vector),这些字符串是命令行参数。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>


int main(int argc,char *argv[])
{
	int i;
	printf("argc=%d\n", argc);
	for ( i = 0; i < argc; i++)
	{
		printf("argv[%d]=%s\n", i,argv[i]);
	}
	return 0;
}

main函数,不传参数运行,如下:
在这里插入图片描述

在编译好的exe文件的目录下,打开cmd:

HelloWorld.exe abc 123

在这里插入图片描述



七. 动态内存申请

1. 动态分配内存的概述

如果数组的长度是预先定义好的,在整个程序中固定不变,但是在实际的编程中,往往会发生这种情况,即所需的内存空间取决于实际输入的数据,而无法预先确定 。为了解决上述问题,C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态的分配内存空间,也可把不再使用的空间回收再次利用。

静态分配
1、在程序编译或运行过程中,按事先规定大小分配内存空间的分配方式。inta[10]
2、必须事先知道所需空间的大小。
3、分配在栈区或全局变量区,一般以数组的形式。
4、按计划分配。

动态分配:
1、在程序运行过程中,根据需要大小自由分配所需空间。
2、按需分配。
3、分配在堆区,一般使用特定的函数进行分配。


2. 动态分配的函数

使用动态分配的函数,需要 #include<stdlib.h>

(1) malloc 函数

函数原型: void* malloc(unsigned int size);

功能说明:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。函数原型返回 void* 指针,使用时必须做相应的强制类型转换 ,分配的内存空间内容不确定,一般使用memset 初始化。

返回值:返回分配空间的起始地址 ( 分配成功 ),返回NULL( 分配失败 ) 。

注意:在调用malloc之后,一定要判断一下,是否申请内存成功。如果多次malloc申请的内存,第1次和第2次申请的内存不一定是连续的。

举例:

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


int main()
{
	int i, * array, n;
	printf("请输入您要申请的数组元素个数\n");
	scanf_s("%d", &n);
	array = (int*)malloc(n * sizeof(int));
	if (array == NULL) 
	{
		printf("申请内存失败\n");
		return 0;
	}
	else 
	{
		memset(array, 0, n * sizeof(int));
		for (i = 0; i < n; i++)
		{
			array[i] = i;
		}
		for (i = 0; i < n; i++) 
		{
			printf("array[%d]=%d\n", i,array[i]);
		}
		free(array);//释放array指向的内存
		return 0;
	}
}

(2) free函数(释放内存函数)
函数原型:void free(void*ptr)
函数说明:free函数释放ptr指向的内存。注意ptr指向的内存必须是malloc、calloc、relloc动态申请的内存。

举例:

char* p=(char*)malloc(100);
free(p);

注意 : free后,因为没有给p赋值,所以p还是指向原先动态申请的内存。但是内存已经不能再用了,p 变成野指针了。一块动态申请的内存只能free一次,不能多次free。

(3) calloc 函数

函数原型:void* calloc(size_t nmemb,size_t size);

函数的功能:在内存的堆中,申请nmemb 块,每块的大小为size个字节的连续区域。

函数的返回值:返回 申请的内存的首地址(申请成功),返回 NULL(申请失败)。

注意:malloc 和 calloc 函数都是用来申请内存的。

区别:

  1. 函数的名字不一样
  2. 参数的个数不一样
  3. malloc 申请的内存,内存中存放的内容是随机的,不确定的,而calloc函数申请的内存中的内容为0
char *p=(char *)calloc(3,100);

上面代码 在堆中申请了3块,每块大小为100个字节,即300个字节连续的区域。

(4) realloc 函数(重新申请内存)

咱们调用malloc和calloc 函数,单次申请的内存是连续的,两次申请的两块内存不一定连续。

有些时候有这种需求,即我先用malloc或者calloc申请了一块内存,我还想在原先内存的基础上挨着继续申请内存。或者我开始时候使用malloc或calloc申请了一块内存,我想释放后边的一部分内存。为了解决这个问题,发明了realloc这个函数。

函数原型:void* realloc(void *s,unsigned int new_size);

函数的功能:在原先s指向的内存基础上重新申请内存,新的内存的大小为 new_size 个字节,如果原先内存后面有足够大的空间,就追加,如果后边的内存不够用,则relloc函数会在堆区找一个new_size 个字节大小的内存申请,将原先内存中的内容拷贝过来,然后释放原先的内存,最后返回新内存的地址。

如果new_size 比原先的内存小,则会释放原先内存的后面的存储空间,只留前面的newsize个字节。

返回值:新申请的内存的首地址。

举例:

char *p;
p=(char *)malloc(100);
//咱们想在100个字节后面追加50个字节
p=(char *)realloc(p,150); //p 指向的内存的新的大小为 150 个字节

注意:malloc、 calloc、 relloc 动态申请的内存,只有在free或程序结束的时候才释放。


3. 内存泄露

内存泄露的概念:申请的内存,首地址丢了,找不了,再也没法使用了,也没法释放了,这块内存就被泄露了。

内存泄露 例1:

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

int main()
{
	char* p;
	p = (char*)malloc(100);
	//接下来,可以用p指向的内存了
	p = "hello world"; // p指向别的地方去了
	//从此以后,再也找不到你申请的100个字节了。则动态申请的100个字节就被泄露了
	return 0;
}

内存泄露 例2:

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

void fun() {
	char* p;
	p = (char*)malloc(100);
	//接下来,可以用p指向的内存了
	p = "hello world"; // p指向别的地方去了
}

int main()
{
	fun();
	fun();
	//从此以后 ,每调用一次fun泄露100个字节
	return 0;
}

内存泄露 解决方案1:

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

void fun() {
	char* p;
	p = (char*)malloc(100);
	//接下来,可以用p指向的内存了
	p = "hello world"; // p指向别的地方去了
	// 释放内存	
	free(p);
}

int main()
{
	fun();
	fun();
	return 0;
}

内存泄露 解决方案2:

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

char* fun() {
	char* p;
	p = (char*)malloc(100);
	//接下来,可以用p指向的内存了
	p = "hello world"; // p指向别的地方去了

	return p;
}

int main()
{
	char* q;
	q = fun();
	//释放内存
	free(q);
	return 0;
}

总结:申请的内存,一定不要把首地址给丢了,在不用的时候一定要释放内存。


八. 字符串处理函数

#pragma指令的作用是:用于指定计算机或操作系统特定的编译器功能。

#pragma warning(disable:4996) 在c文件开始处写上这句话,即告诉编译器忽略4996警告,strcpy、scanf等一些不安全的标准c库函数在vs中可以用了。


## 1. 字符串长度函数

头文件:#include<string.h>
函数定义:size_t strlen(const char *s);

size_t 实际是无符号整型,它是在头文件中,用typedef 定义出来的。

函数功能:测字符指针s指向的字符串中字符的个数,不包括\0

返回值:字符串中字符个数。

举例:

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char str1[20] = "hello";
	char* str2 = "hello";
	printf("%d\n", sizeof(str1)); //20
	printf("%d\n", sizeof(str2));//8
	printf("%d\n", strlen(str1));//5
	printf("%d\n", strlen(str2));//5
	return 0;
}

sizeof 是个关键字,测量数据的占用内存空间大小。如果测量的是数组的名字,则测的是数组占多少个字节。如果sizeof 测的是指针变量,则测的是指针变量本身占几个字节,32平台下结果为4,64位平台下结果为8。

strlen 是个库函数,它测的是字符指针指向的字符串中字符的个数,不管指针是数组的名字,还是个指针变量。


1. 字符串拷贝函数

头文件:#include<string.h>

函数的声明:char* strcpy(char *dest, const char *src);

函数的说明:拷贝src指向的字符串到dest指针指向的内存中,\0也会拷贝。

函数的返回值:目的内存的地址。

注意:在使用此函数的时候,必须保证dest指向的内存空间足够大,否则会出现内存污染。

函数的声明: char* strncpy(char* dest,const char* src,size_t n);

函数的说明:将src指向的字符串前n个字节,拷贝到dest指向的内存中

返回值:目的内存的首地址

注意:strncpy不拷贝\0 , 如果n大于src指向的字符串中的字符个数,则在dest后面填充n-strlen(src)个\0

举例1:

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char buf[100] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
	strncpy(buf, "helloworld", 5); // helloaaaaaaaaaaaaaaaaaaaaaaaa
	printf("%s\n", buf);
	return 0;
}

结果为helloaaaaaaaaaaaaaaaaaaaaaaaa

举例2:

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	int len, i;      
	char buf[100] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
	len = strlen(buf);
	strncpy(buf, "helloworld", 15);
	for (i = 0; i < len; i++)
	{
		printf("%c", buf[i]);
	}
	printf("\n"); // helloworldaaaaaaaaaaaaaaaaaaaaaaa
	return 0;
}

例2验证了:如果n大于src指向的字符串中的字符个数,则在dest后面填充n-strlen(src)个\0


2. 字符串追加函数

头文件:#include<string.h>

函数声明:char* strcat(char* dest,const char* src);

函数功能:strcat函数追加src字符串到dest指向的字符串的后面。追加的时候会追加 \0

注意:保证dest指向的内存空间足够大。

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char str[100] = "aa\0aaaaaaaaaaaaaaaaa";
	char* src = "hello";
	strcat(str, src);
	printf("%s\n", str); // aahello
	return 0;
}

结果是aahello, 验证了追加字符串的时候追加\0

函数声明: char* strncat(char* dest,const char* src,size_t n);

函数功能:追加src指向的字符串的前n个字符,到dest指向的字符串的后面。

注意: 如果n大于src的字符个数,则只将src字符串追加到dest指向的字符串的后面。追加的时候会追加 \0

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char str[20] = "aa\0aaaaaaaaaaaaaaaaa";
	char* src = "hello";
	strncat(str, src, 3);
	printf("%s\n", str); // aahel
	return 0;
}

结果为:aahel, 验证了会追加字符串的时候追加\0


3. 字符串比较函数

头文件:#include<string.h>

函数声明:int strcmp(const char *s1,const char*s2);

函数说明:比较s1和s2指向的字符串的大小。

比较的方法:逐个字符去比较ascII码,一旦比较出大小返回。如果所有字符都一样,则返回0。

返回值:如果s1指向的字符串大于s2指向的字符串返回1,如果s1指向的字符串小于s2指向的字符串返回-1,如果相等的话返回0

函数声明: int strncmp(const char* s1,const char* s2, size_t n);

函数说明:比较s1和s2指向的字符串中的前n个字符

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char* str1 = "helloworld";
	char* str2 = "hellokitty";
	if (strcmp(str1, str2) == 0)
	{
		printf("str1==str2\n");
	}
	else if(strcmp(str1, str2) > 0)
	{
		printf("str1>str2\n");
	}
	else
	{
		printf("str1<str2\n");
	}

	if (strncmp(str1, str2, 5) == 0)
	{
		printf("str1==str2\n");
	}
	else if(strncmp(str1, str2, 5) > 0)
	{
		printf("str1>str2\n");
	}
	else
	{
		printf("str1<str2\n");
	}

	return 0;
}

4. 字符查找函数

头文件:#include<string.h>

函数声明:char* strchr(const char* s, int c);

函数说明:在字符指针s指向的字符串中,找ascii码为c的字符。

注意,是首次匹配,如果过说s指向的字符串中有多个ASCII为c的字符,则找的是第1个字符。

返回值:找到了返回找到的字符的地址,找不到返回NULL。

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char* str = "helloworldhelloworldhelloworld";
	char* p;
	p = strchr(str, 'w');
	if (p == NULL)
	{
		printf("没有您要找的字符\n");
		return 0;
	}
	printf("p-str=%d\n", p-str); // p-str = 5
	return 0;
}

函数声明:char* strrchr(const char* s, int c);

函数的说明:末次匹配,在s指向的字符串中,找最后一次出现的ASCII为c的字符。

返回值:找到了:末次匹配的字符的地址。找不到:返回NULL 。


5. 字符串匹配函数

头文件:#include<string.h>

函数声明:char *strstr(const char * haystack, const char * needle);

函数说明:在haystack 指向的字符串中查找needle指向的字符串,也是首次匹配

返回值:找到了:找到的字符串的首地址 , 没找到:返回NULL。

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char str1[30] = "jfsdjklsd43$#$53jklj$#$4t5";
	char str2[20] = "$#$";
	char* result;
	result = strstr(str1, str2);
	printf("%s\n", result); // $#$53jklj$#$4t5
	printf("%d\n", result-str1); // 11
	return 0;
}

6. 空间设定函数

头文件包含 : #include<string.h>

函数声明:void* memset(void *ptr,int value,size_t num);

函数功能:memset 函数是将ptr指向的内存空间的num个字节全部赋值为value。

参数说明:
ptr:指向任意类型的指针,即指向我们需要修改的内存
value:给 ptr 指向的内存空间的赋的值。
num:确定将ptr所指的内存中的num个字节全都用value代替

返回值:目的内存的首地址,即ptr的值


7. 字符串转换数值

头文件:#include<stdlib.h>

函数的声明:int atoi(const char * nptr);

函数的功能:将nptr 指向的字符串转换成整数返回

返回值:转换后的整数,此值由将输入字符作为数字解析而生成。如果该输入无法转换为该类型的值,则atoi的返回值为 0。

此外,还有atol、atof函数。

int num;
num=atoi(123);
则num的值为123

8. 字符串切割函数

头文件:#include<string.h>

函数声明:char* strtok(char *str, const char *delim);

函数的功能:字符串切割,按照delim指向的字符串中的字符,切割str指向的字符串。其实就是在str指向的字符串中发现了 delim字符串中的字符,就将其变成’\0’, 调用一次strtok只切割一次,切割一次之后,再去切割的时候strtok的第一个参数传NULL,意思是接着上次切割的位置继续切

注意如果str字符串中出现了连续的几个 delim中的字符,则只将第一个字符变成’\0’

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char str[100] = "xiaoming:21,,,.男.女,北京:haidian";
	char* p = ":,.";
	char* q[7];
	int i = 0;
	int j;
	q[i] = strtok(str, p);
	while (q[i] != NULL)
	{
		i++;
		q[i] = strtok(NULL, p);
	}
	for (j = 0; j < i; j++)
	{
		printf("q[%d]:%s\n", j, q[j]);
	}
	printf("str=%p\n", str);
	printf("q[0]=%p\n", q[0]);
	return 0;
}

在这里插入图片描述


9. 格式化字符串操作函数

int sprintf(char* buf,const char* format , …);

输出到buf指定的内存区域。

例:

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char buf[20];
	sprintf(buf, "%d:%d:%d", 2013, 10, 1);
	printf("buf=%s\n",buf); // buf=2013:10:1
	return 0;
}

int sscanf(const char* buf,const char* format , …);

从buf指定的内存区域中读入信息

例:

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	int a, b, c;
	sscanf("2013:10:1", "%d:%d:%d", &a, &b, &c);
	printf("a=%d,b=%d,c=%d\n", a, b, c); // a=2013,b=10,c=1
	return 0;
}

sscanf的高级用法:
1、跳过数据:%*s 或 %*d

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char buf[20];
	sscanf("1234 5678", "%*d%s", buf);//跳过1234,然后隔一个空格获取字符串
	printf("%s\n", buf); // 5678
	return 0;
}

2、读指定宽度的数据:%[width]s

#pragma warning(disable:4996)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <string.h>

int main()
{
	char buf[20];
	sscanf("12345678", "%4s", buf);//从字符串中获取字符串,只要4个字节,存放在buf中
	printf("%s\n", buf); // 1234
	return 0;
}

九. 结构体、共用体、枚举

1. 结构体概念

在程序开发的时候,有些时候我们需要将不同类型的数据组合成一个有机的整体,以便于引用。如:一个学生有学号/姓名/性别/年龄/地址等属性。显然单独多个变量比较繁琐,数据不便于管理,所以在C语言中就发明了结构体类型。

结构体是一种构造数据类型。前面学过一种构造类型——数组。

构造类型:不是基本类型的数据结构也不是指针类型,它是若干个相同或不同类型的数据构成的集合。

数组的概念:描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算。

结构体类型的概念:结构体是一种构造类型的数据结构,是一种或多种基本类型或构造类型的数据的集合。


2. 结构体类型定义

(1). 先定义结构体类型,再去定义结构体变量

struct 结构体类型名{
	成员列表
};

例1:

struct stu {
	int num;
	char name[20];
	char sex;
};
// 有了结构体类型后,就可以用类型定义变量了
struct stu lucy,bob,lilei;//定义了三个structstu类型的变量
//每个变量都有三个成员,分别是numname sex

咱们可以暂时认为结构体变量的大小是它所有成员之和。

(2).在定义结构体类型的时候顺便定义结构体变量,以后还可以定义结构体变量

struct结构体类型名{
	成员列表;
}结构体变量1,变量2;

例2:

 struct stu{
 int num;
 char name[20];
 char sex;
 }lucy,bob,lilei;
 
 struct stu xiaohong,xiaoming;

(3) 在定义结构体类型的时候,没有结构体类型名,顺便定义结构体变量。因为没有类型名,所以以后不能再定义相关类型的数据了。

struct {
   成员列表;
 }变量1,变量2;

例3:

struct{
  int num;
  char name[20];
  char sex;
 }lucy,bob;

以后没法再定义这个结构体类型的数据了,因为没有类型名。

(4) 最常用的方法。通常咱们将一个结构体类型重新起个类型名,用新的类型名替代原先的类型

步骤1:先用结构体类型定义变量

 typedef struct stu{
 int num;
 char name[20];
 char sex;
 }STU;

以后STU 就相当于 struct stu, STU lucy; 和 struct stu lucy;是等价的。


3. 结构体变量的定义初始化及使用

(1)、结构体变量的定义和初始化
结构体变量,是个变量,这个变量是若干个相同或不同数据构成的集合。

注意:
1):在定义结构体变量之前首先得有结构体类型,然后再定义变量。
2):在定义结构体变量的时候,可以顺便给结构体变量赋初值,被称为结构体的初始化。
3):结构体变量初始化的时候,各个成员顺序初始化。

例子:

struct stu{
  int num;
  char name[20];
  char sex;
 };
 
 struct stu boy;
 
 struct stu lucy={
  101,
  "lucy",
  'f'
 };

(2)、结构体变量的使用

结构体变量成员的引用方法: 结构体变量.成员名

例子:

struct stu{
  int num;
  char name[20];
  char sex;
 };

struct stu bob;

// bob是个结构体变量,但是bob.num是个int类型的变量
bob.num=101;

// bob.name是个字符数组,是个字符数组的名字,代表字符数组的地址,是个常量
// 下面不可行
bob.name="bob"; //是不可行,是个常量

// 可行的代码:
strcpy(bob.name,"bob");

结构体成员多级引用:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

struct date
{
	int year;
	int month;
	int day;
};

struct stu
{
	int num;
	char name[20];
	char sex;
	struct date birthday;
};

int main()
{
	struct stu lilei = { 101,"lilei",'m' };
	lilei.birthday.year = 2024;
	lilei.birthday.month = 10;
	lilei.birthday.day = 8;
	printf("%d%s%c\n", lilei.num, lilei.name, lilei.sex);
	printf("%d%d%d\n", lilei.birthday.year, lilei.birthday.month, lilei.birthday.day);
	return 0;
}

(3) 相同类型的结构体变量可以相互赋值

注意:必须是相同类型的结构体变量,才能相互赋值。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

struct stu
{
	int num;
	char name[20];
	char sex;
};

int main()
{
	struct stu bob = { 101,"bob",'m' };
	struct stu lilei;

	lilei = bob;
	printf("%d %s %c\n", lilei.num, lilei.name, lilei.sex); // 101 bob m
	return 0;
}

4. 结构体数组

结构体数组是个数组,由若干个相同类型的结构体变量构成的集合。

(1) 结构体数组的定义方法
struct 结构体类型名 数组名[元素个数];

举例:

struct stu{
 int num;
 char name[20];
 char sex;
 };

// 定义了一个structstu类型的结构体数组edu
// 这个数组有3个元素分别是edu[0]、edu[1]、edu[2]
struct stu edu[3]; 

结构体数组元素的引用 :数组名[下标]

(2) 数组元素的使用

举例:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef struct student
{
	int num;
	char name[20];
	float score;
}STU;

STU edu[3] = {
	{101,"Lucy",78},
	{102,"Bob",59.5},
	{103,"Tom",85}
};

int main()
{
	int i;
	float sum = 0;
	for ( i = 0; i < 3; i++)
	{
		sum += edu[i].score;
	}
	printf("平均成绩为 %f\n", sum / 3);
	return 0;
}

5. 结构体指针

结构体指针即结构体的地址,结构体变量存放内存中,也有起始地址。咱们定义一个变量来存放这个地址,那这个变量就是结构体指针变量。结构体指针变量也是个指针,既然是指针在32位环境下,指针变量的占4个字节,存放一个地址编号。

(1) 结构体指针变量的定义方法
struct 结构体类型名* 结构体指针变量名;

举例:

 struct stu{
 	int num;
 	char name[20];
 };

 struct stu* p; //定义了一个struct stu*类型的指针变量
 struct stu boy;
 p = &boy;

(2) 访问结构体变量的成员方法

 boy.num=101; //可以,通过结构体变量名.成员名
 (*p).num=101; //可以,*p相当于p指向的变量boy
 p->num=101; //可以,指针->成员名

通过结构体指针来引用指针指向的结构体的成员,前提是指针必须先指向一个结构体变量。


6. 结构体指针应用场景

(1):保存结构体变量的地址

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef struct student
{
	int num;
	char name[20];
	float score;
}STU;

int main()
{
	STU* p, lucy;
	p = &lucy;
	p->num = 101;
	strcpy(p->name, "baby");
	// 如果p->name="baby";这么写是错误的
	// 因为p->name相当于lucy.name是个字符数组的名字,是个常量
	return 0;
}

(2):传结构体变量的地址

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef struct student
{
	int num;
	char name[20];
	float score;
}STU;

void fun(STU* p)
{
	p->num = 101;
	(*p).score = 87.6;
	strcpy(p->name, "lucy");
}

int main()
{
	STU girl;
	fun(&girl);
	printf("%d %s %f\n", girl.num, girl.name, girl.score); // 101 lucy 87.599998
	return 0;
}

(3):传结构体数组的地址
结构体数组,是由若干个相同类型的结构体变量构成的集合。存放在内存里,也有起始地址,其实就是第0个结构体变量的地址。

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef struct student
{
	int num;
	char name[20];
	float score;
}STU;

void fun(STU* p)
{
	p[1].num = 101;
	(*(p + 1)).score = 88.6;
}

int main()
{
	STU edu[3];
	fun(edu);
	printf("%d %f\n", edu[1].num, edu[1].score); // 101 88.599998
	return 0;
}

注意:
(1):结构体变量的地址编号和结构体第一个成员的地址编号相同,但指针的类型不同

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef struct student
{
	int num;
	char name[20];
	float score;
}STU;

int main()
{
	STU bob;
	printf("%p\n", &bob) // 0000003106CFFBD8
	printf("%p\n", &(bob.num)); // 0000003106CFFBD8
	return 0;
}

(2):结构体数组的地址就是结构体数组中第0个元素的地址

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef struct student
{
	int num;
	char name[20];
	float score;
}STU;

int main()
{
	STU edu[3];;
	printf("%p\n", edu);//struct stu* 0000005C756FFAD0
	printf("%p\n", &(edu[0]));//struct stu* 0000005C756FFAD0
	printf("%p\n", &(edu[0].num));//int * 0000005C756FFAD0
	return 0;
}

7. 共用体

共用体和结构体类似,也是一种构造类型的数据结构。既然是构造类型的,咱们得先定义出类型,然后用类型定义变量。定义共用体类型的方法和结构体非常相似,把struct改成union就可以了。

在进行某些算法的时候,需要使几种不同类型的变量存到同一段内存单元中,几个变量所使用空间相互重叠。这种几个不同的变量共同占用一段内存的结构,在C语言中,被称作“共用体”类型结构。共用体所有成员占有同一段地址空间,共用体的大小是其占内存长度最大的成员的大小。

共用体的特点:
1、同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用
2、共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖
3、共用体变量的地址和它的各成员的地址都是同一地址
4、共用体变量的初始化只能为第一个成员赋值,不能给所有成员都赋初

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef union data{
char a;
int b;
}DATA;

int main()
{
	DATA temp;
	temp.b = 0xffffffff;
	printf("temp.b=%x\n", temp.b); // temp.b=ffffffff
	temp.a = 0x0d;
	printf("temp.a=%x\n", temp.a); // temp.a=d
	printf("temp.b=%x\n", temp.b); // temp.b=ffffff0d
	return 0;
}

8. 枚举

将变量的值一一列举出来,变量的值只限于列举出来的值的范围内,枚举类型也是个构造类型的,类型定义类似结构体类型的定义。使用枚举的时候,得先定义枚举类型,再定义枚举变量。

(1)枚举类型的定义方法

enum 枚举类型名{
	枚举值列表;
};

在枚举值表中应列出所有可用值,也称为枚举元素。枚举元素是常量,默认是从0开始编号的,枚举变量仅能取枚举值所列元素。

(2)枚举变量的定义方法
enum 枚举类型名 枚举变量名;

// 枚举类型
enum week
{
	one,two,three,four
};

// 枚举变量
enum week num1;

num1 = one; // 正确
num1 = six;// 错误,枚举列表中没有six

① 枚举值是常量 , 不能在程序中用赋值语句再对它赋值。 如:one=2等都不可以。

② 枚举元素本身由系统定义了一个表示序号的数值。默认是从0开始顺序定义为0,1,2… 。如:在上面的代码中,one=0,two=1。

③ 可以改变枚举值的默认值:如

enum week
{
	one=1,two,three,four=4
};

十. 文件操作

1. 文件的概念

凡是使用过文件的人对文件都不会感到陌生
在这里插入图片描述
文件用来存放程序、文档、音频、视频数据、图片等数据的。文件就是存放在磁盘上的,一些数据的集合。

在windows 下可以通过写字板或记事本打开文本文件对文件进行编辑保存。写字板和记事本是微软程序员写的程序,对文件进行打开、显示、读写、关闭。

文件的分类:

(1)磁盘文件:(我们通常认识的文件)指一组相关数据的有序集合,通常存储在外部介质(如磁盘)上,使用时才调入内存。

(2)设备文件:在操作系统中把每一个与主机相连的输入、输出设备看作是一个文件,把它们的输入、输出等同于对磁盘文件的读和写。键盘:标准输入文件。屏幕:标准输出文件。

在Linux 操作系统中,每一个外部设备都在/dev目录下对应着一个设备文件,咱们在程序中要想操作设备,就必须对与其对应的/dev下的设备文件进行操作。

标准io库函数对磁盘文件的读取特点
在这里插入图片描述
文件缓冲区是库函数申请的一段内存,由库函数对其进行操作,程序员没有必要知道存放在哪里,只需要知道对文件操作的时候的一些缓冲特点即可。

VS 中对普通文件的读写是全缓冲的。

全缓冲:标准io库函数 ,往普通文件读写数据的,是全缓冲的。

刷新缓冲区的情况
1.缓冲区满了,刷新缓冲区
2.调用函数刷新缓冲区 fflush(文件指针)
3.程序结束 会刷新缓冲区


2. 文件指针

文件指针在程序中用来标识(代表)一个文件的,在打开文件的时候得到文件指针,文件指针就用来代表咱们打开的文件。我们对文件进行读、写、关闭等操作的时候,对文件指针进行操作即可,即咱们将文件指针,传给读、写、关闭等函数,那些函数就知道要对哪个文件进行操作。

定义文件指针的一般形式为: FILE* 指针变量标识符;

FILE 为大写,需要包含<stdio.h> , FILE 是系统使用typedef定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息。

一般情况下,我们操作文件前必须定义一个文件指针标识我们将要操作的文件。实际编程中使用库函数操作文件,无需关心FILE结构体的细节 ,只需要将文件指针传给io库函数,库函数再通过FILE结构体里的信息对文件进行操作。

FILE 在stdio.h 文件中的文件类型声明:

typedef structshort level; //缓冲区“满”或“空”的程度
 	unsigned flags; //文件状态标志
    char fd; //文件描述符
 	unsigned charhold; //如无缓冲区不读取字符
    short bsize; //缓冲区的大小
    unsigned char *buffer; //数据缓冲区的位置
    unsigned ar*curp; //指针,当前的指向
    unsigned istemp; //临时文件,指示器
    short token; //用于有效性检查
}FILE;

在缓冲文件系统中 , 每个被使用的文件都要在内存中开辟一块 FILE 类型的区域 , 存放与操作文件相关的信息
在这里插入图片描述
对文件操作的步骤:
1、对文件进行读写等操作之前要打开文件得到文件指针
2、可以通过文件指针对文件进行读写等操作
3、读写等操作完毕后,要关闭文件,关闭文件后,就不能再通过此文件指针操作文件了

补充:
c 语言中有三个特殊的文件指针无需定义,在程序中可以直接使用
stdin: 标准输入 默认为当前终端(键盘),我们使用的scanf、getchar 函数默认从此终端获得数据
stdout:标准输出 默认为当前终端(屏幕),我们使用的printf、puts函数默认输出信息到此终端
**stderr:**标准错误输出设备文件 默认为当前终端(屏幕),当我们程序出错使用:perror函数时信息打印在此终端

总结:文件指针是个指针,它是个FILE类型结构体指针,用文件指针来标识一个文件。


3. 打开文件 fopen

函数的声明:FILE* fopen(const char *path, const char *mode);

函数说明:fopen 函数的功能是打开一个已经存在的文件,并返回这个文件的文件指针(文件的标识)或者创建一个文件,并打开此文件,然后返回文件的标识。

函数的参数:

参数1: 打开的文件的路径,可以是绝对路径或相对路径。
参数2:文件打开的方式,即以什么样的方式(只读、只写、可读可写等等)打开文件

第二个参数的几种形式(打开文件的方式):

参数作用
r以只读方式打开文件,该文件必须存在。
r+以读/写方式打开文件,该文件必须存在。
rb+以读/写方式打开一个二进制文件,只允许读/写数据。
rt+以读/写方式打开一个文本文件,允许读和写。
w打开只写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。
w+打开可读/写文件,若文件存在则文件长度清为零,即该文件内容会消失;若文件不存在则创建该文件。
a以附加的方式打开只写文件。若文件不存在,则会创建该文件;如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留(EOF 符保留)。
a+以附加方式打开可读/写的文件。若文件不存在,则会创建该文件,如果文件存在,则写入的数据会被加到文件尾后,即文件原先的内容会被保留(EOF符不保留)。
wb以只写方式打开或新建一个二进制文件,只允许写数据。
wb+以读/写方式打开或新建一个二进制文件,允许读和写。
wt+
以读/写方式打开或新建一个文本文件,允许读和写。
at+以读/写方式打开一个文本文件,允许读或在文本末追加数据。
ab+
以读/写方式打开一个二进制文件,允许读或在文件末追加数据。

比如:如果我们现在想打开一个D盘根目录下的abc.dat,并且想读出该文件里的数据,那么我们可以这样写:

FILE *fp;
fp=fopen("d:\\abc.dat","r")
//后面通过fp指针开始读文件

其他关于文件操作的一些函数,用到时,网上查找就好,这里就不过多赘述,再见。


十一. 补充知识点

1. exit()函数

exit的功能为:退出当前运行的程序,并将参数value返回给主调进程。

exit(0),exit(1) 和 exit(-1)的区别 :
(1) exit(0)表示程序正常退出;除了0之外,其他参数均代表程序异常退出,如:exit(1),exit(-1)。
(2) exit(1)和exit(-1)是分别返回1和-1到主调程序。
(3) exit(0)则是返回0。exit(0)表示程序正常退出,非0表示异常退出。

return与exit的区别: return是语言级别的,它表示了调用堆栈的返回,而exit是系统调用级别的,它表示了一个进程的结束。

在main中:return v; 与 exit(v); 的效果相同。但是在其它功能函数中就会有所区别:return会跳出函数,而exit会结束程序。

通常可以借助exit()的返回值判断程序结束状态,0表示程序正常退出, 其它值是异常退出, 可以在退出前可以给出一些提示信息,方便在调试程序时察看出错原因。



Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐