Linux 操作系统和C语言(详解)

一、Linux操作系统

(一)认识Linux操作系统

1、操作系统

定义:本质是运行在计算机上的软件程序

组成:内核 + 外壳(图形化界面+软件工具...)

作用:向用户提供操作接口,管理计算机硬件和软件资源。

主流操作系统有Windows、 MacOS、 Linux

2、GNU/Linux Linux1.0

1.Linux又称为类Unix操作系统 Minux

2.Linux的特点免费、开源、可裁剪、移植性好。

3.1991年由林纳斯·托瓦兹完成发布。

4.Linux严格意义来讲是操作系统的内核。

问题1:Linux和Unix的关系?

  • UNIX是一个多用户和多任务操作系统,而Linux是基于UNIX的操作系统。
  • Linux是开源的,免费使用的;而Unix不是开源的,是授权的操作系统。
  • Linux使用范围更广泛,从台式机,服务器,智能手机到大型机,而Unix主要用于服务器,工作站或PC

问题2、Linux和GNU的关系?

        Linux操作系统是GNU与Linux内核的完美结合,他们都遵守GPL协议,属于开源代码。)GNU是一个自由软件工程项目,GNU软件都遵守一套称为GPL的协议,该协议规定了GNU软件必须开放源码。.

问题3、Linux和Ubuntu的关系?

        Linux这个词是指操作系统的内核,ubuntu是指基于这种内核的操作系统,就是在linux这个内核上又加上了一种界面系统,就像你看到的windows的界面一样。. 而因为 Linux是开放源代码的,所以网上会出现各种各样的发行版本,Ubuntu Linux只是其中一种。. Ubuntu采用Linux内核,图形界面采用GNOME(Kubuntu使用KDE)。. 简而言之,Linux系统是个统称,它有RedHat、Debian、Suse、Ubuntu等发行版本,它们都是用的Linux内核,都是Linux系统。. 这两个最大的区别在包管理模式上。.

3、Ubuntu操作系统

终端:命令行解释器

linux @ ubuntu : ~ $

用户名 @ 操作系统名 :当前路径 命令提示符

hqyj@CentOS:~$

/ :根目录

/home :存放所有普通用户主目录

/home/hq :~ 家目录(当前用户主目录)

pwd :查看当前绝对路径

4、Linux文件管理形式

windows:分盘符 C\D\E\F...

linux:倒置树形结构管理文件 FHS标准

根目录:

/bin: 二进制文件,存放普通用户的命令。

/dev: 系统的设备文件,设备驱动程序。

/home:存放用户目录

/lib: 存放与系统运行相关的库文件

/mnt: 挂载目录

/etc: 系统配置文件

/root:超级用户目录

/boot:引导linux启动的核心文件

/lost-found:这个目录平时是空的,当系统非正常关机而留下的“无家可归”的文件便会储存在这里

/misc:储存着一些特殊的字符的定义

/proc:存放着用户与内核的交互信息

/sbin:系统的管理命令,这里存放的是系统管理员使用的程序

/srv: 系统启动服务时可以访问的数据库目录

/tmp: 临时文件,重启后自动清空

/var: 某些大文件的溢出区,比如各种服务的日志文件

/media:存放着可移除的设备,比如软盘,光盘

/opt: (option : 自由选择)主要给源码安装软件时选择的安装目录位置

/selinux:主要用来加固操作系统,提高系统的安全性

/sys:管理设备文件

/usr:最大的目录,存放着应用程序和文件

5、用户操作

1)用户切换

su + 用户名

root :超级用户

Linux:普通用户

su root :切换到root用户,具备管理员权限。

su linux:切换到linux用户

2)创建用户

adduser 用户名 //执行时需要获取管理员权限

root@ubuntu:/home/linux# adduser faright

Adding user `faright' ...

Adding new group `faright' (1002) ...

Adding new user `faright' (1002) with group `faright' ...

Creating home directory `/home/faright' ...

Copying files from `/etc/skel' ...

Enter new UNIX password: //给新用户输入密码

Retype new UNIX password: //重复确认密码

passwd: password updated successfully //密码更新成功Z

Changing the user information for faright //输入用户信息

Enter the new value, or press ENTER for the default

Full Name []:

Room Number []:

Work Phone []:

Home Phone []:

Other []:

Is the information correct? [Y/n] y //信息是否正确? y是 n否

3)删除用户

sudo userdel 用户名 : 仅删除用户

sudo userdel -r 用户名 : 删除用户及用户目录

//执行时需要获取管理员权限

sudo工具

sudo + 命令 :在执行本条命令时获取管理员权限

4)修改用户密码

sudo passwd 用户名 :修改用户密码

linux@ubuntu:~$ sudo passwd linux

[sudo] password for linux: //输入原密码

Enter new UNIX password: //输入新密码

Retype new UNIX password: //重复确认

passwd: password updated successfully //密码更新成功

6、文件操作

1. ls list列表

基本功能:列出当前路径下的文件

ls -a : 查看当前路径下的所有文件(包含隐藏文件)

ls -i : 查看文件的inode号 (每个文件都有唯一的身份编号)

ls -l :查看文件的详细信息

d rwxr-xr-x 8 linux linux 4096 Sep 17 17:02 ARM

[文件类型] [文件权限] [硬链接数] [用户名] [组名] [文件大小] [最后修改日期] [文件名]

1)文件类型 (7种)

- 普通文件 - .c .txt .zip .exe .bin .jpg .mp3

d 目录文件 - 文件夹

l 链接文件 - 快捷方式

s 套接字文件 - 网络编程

p 管道文件 - 进程线程

b 块设备文件

c 字符设备文件 - 驱动开发

2)文件权限

r : 可读权限 4

w :可写权限 2

x :可执行权限 1

- :没有权限 0

rwx r-x r-x

[当前用户权限] [组内成员权限] [其他用户权限]

111 101 101

7 5 5

chmod 权限值 文件名

练习:

将Music的文件夹权限修改为当前用户可读可写不可执行,

组内成员仅可读,其他用户可写可执行。

-wx r-- -wx

3 4 3

chmod 643 Music

2.cd change directory 改变路径

切换路径

cd 绝对路径 :从根目录开始索引

linux@ubuntu:~$ cd /home/linux/Music/

cd 相对路径 :相对于当前路径进行索引

linux@ubuntu:~$ cd ./Music

. 当前路径

.. 上一级路径 (..的inode号和上一级路径的inode号相同)

3.touch

touch 普通文件名.后缀 : 新建普通文件

touch 1.c 2.jpg 3.mp3 4.mp4 5.zip :同时创建多个文件

touch创建重名的普通文件会更新文件的最后修改时间。

4.mkdir

mkdir 目录文件名 : 新建目录文件

mkdir 创建重名的目录文件会报错:: File exists

mkdir -m 权限值 目录文件名 :指定权限创建目录文件

mkdir -p 文件夹1/文件夹2/文件夹3.... :创建具有层级关系的目录

文件的默认权限:

umask文件掩码 0002

目录文件的默认权限 : 0777 - 0002 = 0775

普通文件的默认权限 : 0666 - 0002 = 0664

5.rm

rm 普通文件名 :删除普通文件

rm -r 目录文件名 :删除目录文件

rm -rf 目录文件名 :强制删除受写保护的空目录文件

sudo rm -rf 目录文件名 :强制删除受写保护的非空目录文件

rm *.格式 :指定删除某一类文件 *通配符

rm *.txt : 删除所有txt格式的文件

rm * -r :删除当前路径下的普通文件和目录文件

sudo rm !(3.txt) -rf :删除3.txt以外的所有文件

6.cp

cp 普通文件 目标路径 :将普通文件复制并粘贴到目标路径下

cp 普通文件 新文件名 :将普通文件另存为新文件名

cp -r 目录文件 目标路径 :将目录文件复制并粘贴到目标路径下

7.mv

mv 文件名 目标路径 :将指定文件移动到目标路径

mv 原文件名 新文件名 :重命名

7、常用快捷键

打开终端:

ctrl + alt + t :打开一个新的终端默认在家目录下

ctrl + shift + n :打开与已打开终端处于相同路径的终端(光标)

关闭终端:

ctrl + d

放大终端:

ctrl + shift + “+”

缩小终端:

ctrl + “-”

清屏:

ctrl + l

clear命令

查看历史命令:

上下键

补全:

Tab

8、vi编译器

一款基于命令行的文档编辑器

vi + 文件名 : 用vi编辑器打开文件

三种工作模式:

命令行模式:文本复制、粘贴、撤销、恢复、删除...

插入模式:文本编辑

底行模式:保存、退出、查找、替换、分屏...

工作模式切换:

1.打开文档默认处于命令行模式

2.命令行模式下通过a、A、i、I、o、O切换到插入模式(insert)

3.先按Esc返回命令行模式,通过shift+":"切换到底行模式

命令行模式:

单行复制 yy

多行复制 nyy (n为行数)

单行剪切 dd

多行剪切 ndd

粘贴 p

撤销 u

恢复 ctrl + r

移动光标:

gg 光标移动到首行

G 光标移动到末行

0 光标移动到行首

$ 光标移动到行尾

整理代码格式 gg = G

插入模式:

a :在光标所在位置后面键入

A :在光标所在行行尾键入

i :在光标所在位置前面键入

I :在光标所在行行首键入

o :在光标所在行下面新建一行键入

O :在光标所在行上面新建一行键入

底行模式:

保存 : w

退出 : q

保存并退出: wq

强制操作 : !

强制保存 : w!

强制退出 : q!

指定行复制: 1,10y (复制1-10行内容)

指定行剪切: 1,10d

查找 : /str (str即为要查找的字符串) n键向下翻找

取消高亮 : noh

替换:

s/str1/str2 : 将光标所在行首个str1替换成str2

s/str1/str2/g : 将光标所在行所有str1替换成str2

1,100s/str1/str2/g : 将1-100行所有str1替换成str2

1,$s/str1/str2/g : 将全文所有str1替换成str2

分屏: vsp 文件名

全部保存并退出 : wqa

9、简单C编程步骤

1)创建C程序文件

touch hello.c

2)用vi编辑器打开文件进行编程

vi hello.c

3)编写代码

#include<stdio.h> //头文件

int main(int argc, const char *argv[]) //主函数

{

printf("HelloWorld!\n"); //打印语句

return 0; //返回值

}

4)编译

将C程序文件编译生成机器能够识别的二进制可执行程序

gcc hello.c

5)执行可执行程序

./a.out //默认可执行文件名为a.out

linux@ubuntu:~/21101$ gcc hello.c -o abc //生成指定的可执行文件名

linux@ubuntu:~/21101$ ls

abc a.out day1.txt hello.c passwd.txt

linux@ubuntu:~/21101$ ./abc

HelloWorld!

(二)编译器

1、作用

将高级语言编译生成机器能够识别的二进制语言。

2、编程语言发展阶段

第一代计算机语言

机器语言

纸带打孔编程 0不打孔 1打孔

二进制序列 010101101010010101011

优点:

1.能够被机器直接运行

2.灵活速度快

缺点:

1.不便于阅读

2.难以记忆编写效率低

3.很难确保程序的正确性

4.移植性差,重用性差

第二代计算机语言

汇编语言

用一些容易理解和记忆的缩写单词来代替一些特定的指令

MOV R1,#1

MOV R2,#2

ADD R3,R1,R2

接近人类的自然语言,提高了编码效率,但是针对寄存器进行编程,依赖于特定的计算机硬件,导致兼容性差。

第三代计算机语言

高级语言

C、C++、Java、Python....

3、gcc四步编译过程

1.预处理

展开头文件,删除注释、空行等无用内容,替换宏定义。

gcc -E hello.c -o hello.i

099a849692b226d283a33256ec385f5a.png

33475f8ce184c5f8003120b94eb298cd.png

2.编译

检查语法错误,如果有错则报错,没有错误则生成汇编文件。

gcc -S hello.i -o hello.s

feff6757b85840499170434651ecde11.png

3.汇编

将汇编文件生成二进制目标文件

gcc -c hello.s -o hello.o

863faab616ead2d0557251ca38d18dec.png

9a082d6c1506e15069cb22f2005d124a.png

4. 链接

将目标文件链接库文件,最终生成机器能够运行的二进制可执行程序。

gcc hello.o -o hello

18f62bf315cfa21541380e0b6d86af94.png

6692ea24e27e1fc7be3c4abf1c06fa10.png

4、计算机内存表示

计算机中最小的内存单位是bit位

而计算机内存中的基本单位是byte字节

1byte字节 = 8bit位

1KB = 1024Byte

1MB = 1024KB

1GB = 1024MB

1TB = 1024GB

1PB = 1024TB


二、C基础

(一)计算机数据表示

任何数据都是以二进制的形式存储到计算机中的。

1、数据类型取值范围

数据存储形式:

1.所有数据都是以二进制补码的形式存储到内存中的。

2.正数的原码、反码、补码都一样

3.负数的原码保留符号位,其余位取反得到反码,反码+1得到补码。

数据类型:变量占用内存大小

sizeof(变量名或类型名) = 占用内存大小

类型名 字节数(32bit-OS) 取值范围
char 字符型 1字节 -2^7 ~ 2^7 -1
short 短整型 2字节 -2^15 ~ 2^15-1
int 整 型 4字节 -2^31 ~ 2^31-1
long 长整型 4字节 -2^31 ~ 2^31-1
float 单精度浮点型 4字节 -3.4*10^-38 ~3.4*10^38
double 双精度浮点型 8字节 -1.7*10^-308~1.7*10^308

以char为例

1字节 = 8位

unsigned 无符号修饰

unsigned char 无符号char类型

8位数据位

最小:0000 0000

最大:1111 1111

取值范围:0 ~ 2^8-1

signed 有符号修饰 (默认)

1位符号位+7位数据位

符号位为0表示正数,符号位为1表示负数

(signed) char 有符号char类型

最小:1000 0000 (特殊:符号位和数据位重合)

最大:0111 1111

取值范围: -2^7 ~ 2^7-1

(signed) char ch = 130

正数原码 1000 0010

正数补码 1000 0010

负数补码 1000 0010

负数原码 1111 1110

= -126

2、数值型数据

二进制(BIN) 0 1 10 11 100 101 110 111 1000 ...

八进制(OCT) 0 - 7 10 - 17 20 - 27 ... 77 100 ...

十进制(DEC) 0 - 9 10 - 19 20 - 29 ... 99 100 ...

十六进制(HEX) 0 - 9 a - f 10 - 1f 20 - 2f ... ff 100 ...

各进制的标识:

二进制 0b

八进制 0

十六进制 0x

进制之间的转换形式:

十进制转二进制

1)短除法:除2倒取余数

66 => 1000010

98 => 1100010

2)拆分法:拆成2的次方和

66 => 64 + 2 => 2^6 + 2^1 => 1000010

98 => 64 + 32 + 2 => 2^6 + 2^5 + 2^1 => 1100010

3)计算器法

开始菜单 - 计算器 - 程序员

二进制转十进制

10011001 => 2^7 + 2^4 + 2^3 + 2^0 = 128 + 16 + 8 + 1 = 153

八进制转二进制

一位八进制最大为7需要最大的三位二进制来表示 111

一位八进制对应三位二进制 (421码)

067 => 110 111

0153 => 001 101 011

0526 => 101 010 110

二进制转八进制

从低位开始三位一组表示一位八进制

1011001101 => 001 011 001 101 => 01315

10111101 => 010 111 101 => 0275

十六进制转二进制

一位十六进制最大为f需要最大的四位二进制表示1111

一位十六进制对应四位二进制(8421码)

0xab => 1010 1011

0x3ac => 0011 1010 1100

0xbf8 => 1011 1111 1000

二进制转十六进制

从低位开始四位一组表示一位十六进制

100111010110 => 1001 1101 0110 => 0x9d6

11010110001 => 0110 1011 0001 => 0x6b1

3、非数值型数据

'!' '#' '%'

ascii码(美国信息交换标准代码)

空 ‘\0’

换行 ‘\n’

回车 ‘\r’

'\\' '\"'

9512168ccee641286b0a8a72690e3c8f.png

字符0 48

字符9 57

'A' 65

'Z' 90

'a' 97

'z' 122

(二)变量

概念: 在程序运行过程中其值会发生变化的量

定义变量:

[存储类型] [数据类型] 变量名;

(auto) int num;

存储类型:变量存储位置

1、auto(自动型)

在缺省存储类型定义变量时,一般将存储类型默认为auto类型。

2、static(静态型)

延长生命周期,限制作用域。

static修饰局部变量:

未初始化初值为0

存储位置:全局区(静态区)

生命周期:同程序共存亡

9745f378919aa7809820e331c15b9b32.png

static修饰全局变量:

将全局变量的作用域限制在了本文件中

2b8ac9bba63fed3a11f869c75606275d.png

3、extern(外部引用类型)

在使用同一程序不同文件的全局变量,通常加extern进行外部引用。

95cbadd2a7c246a54160532a08abe780.png

extern引用语句放在全局,引用到该文件的所有的函数均可访问这个全局变量。

extern引用语句放在局部,仅能在所在函数体内部使用。

b0251ae1bf01238aecc5fee7b8bc2e09.png

4、register(寄存器类型)

寄存器是集成在CPU内部的一块很小的存储空间,而内存是挂载在CPU的数据总线 上的大型存储设备。

当一个变量需要被反复多次读取时,为了提高程序执行效率,通常可将该变量定义成寄存器类型。

类型名 字节数(32bit-OS) 取值范围
char 字符型 1字节 -2^7 ~ 2^7 -1
short 短整型 2字节 -2^15 ~ 2^15-1
int 整 型 4字节 -2^31 ~ 2^31-1
long 长整型 4字节 -2^31 ~ 2^31-1
float 单精度浮点型 4字节 -3.4*10^-38 ~3.4*10^38
double 双精度浮点型 8字节 -1.7*10^-308~1.7*10^308

5、局部变量与全局变量

变量分类:

局部变量

定义位置:定义在函数体内部

存储位置:栈区

全局变量

定义位置:定义在函数体外部

存储位置:全局区

区别1:全局变量和局部变量未初始化初值不同

2858736c3660b1f721a8d7a60c4b7b8f.png

区别2:全局变量和局部变量存储位置不同

757f565b490d2f1c770a311ec04b46b0.png

区别3:全局变量和局部变量的生命周期不同

局部变量在函数执行结束后就被释放,全局变量在整个程序执行结束后才会被释放。

530c34ede5a47592f89dc626d73e40e0.png


(三)常量

概念:在程序运行过程中其值不会发生变化的量

分类:

1、字符型常量

标识: '    '

一般用char类型的变量来存储字符型常量

'A'   'Z'   '\\'   ‘\'    '\0'

' !'

转义字符: ‘\041’   ‘\x21'

2、字符串常量

标识: ""

"hello" "a" "0"

字符串都以'\0'作为结束标志

一般用char类型的数组来存储字符串常量

char ch[10] = "hello";

3、整型常量与强制类型转换

整型常量:

整数 0 100 1000

一般用short、int、long、long int、long long类型的变量来存储整型常量

浮点型常量

小数 0.0 3.1415926

一般用float、double类型的变量来存储浮点型常量

强制类型转换

格式:

(要转换的类型名)变量名;

注意:隐式转换

char > short > int > long > float > double

示例1:

int a = 3,b = 2;

float num = (float)a/b; //将变量a强制转换成float类型

示例2:

int num = 0x12345678;

char ch = (char)num; //0x78 高字节数据向低字节数据强转时会导致数据丢失

4、指数常量

3*10^8 => 3E+8

2*10^-5 => 2E-5

一般用double类型的变量来存储指数型常量

5、标识常量

宏定义:起标识作用

#define 宏名 常量或表达式

#define PI 3.1415926

特点:先替换再计算

例:

#define N 2

#define M N+3

#define NUM N+M/2+1

int main()

{

printf("NUM = %d\n",NUM); // 6 5 6.5 5.5

return 0;

}

(四)C语言的词法符号

概念:在程序设计语言中,由若干个字符所构成的有意义的最小语法单位。

分类:关键字、标识符、运算符、分隔符、标点符号

1、关键字

概念:由系统预定义的具有特定功能的词法符号,不得由用户更改。

分类:

存储类型:auto、static、extern、register

数据类型:char、short、int、long、float、double、signed、unsigned

构造类型:struct(结构体)、union(共用体)、enum(枚举类型)

分支结构:if、else、switch、case、default

循环结构:for、while、do、goto、break、continue

其他:return(返回)、void(空类型)、sizeof(占用空间大小)、typedef(类型重定义)、const(只读修饰)、volatile(防止编译器优化)

补充说明:

typedef

作用:重定义类型名(起别名)

格式: typedef 原类型名 别名;

示例:

#include<stdio.h>

typedef unsigned int uint;

int main()

{

uint num = 10;

printf("%u\n",num);

return 0;

}

2、标识符

概念:由用户自定义的具有标识作用的词法符号,例如:变量名、函数名、宏名...

标识符命名规则:

1.由数字、字母、下划线组成

2.不能以数字开头

3.不能与关键字重名

3、运算符(算术、逻辑、位、关系、赋值、条件)

3.1、 算术运算符

+ - * / % ++ --

/ :整数相除要取整

% :取余运算只能是整数运算

10 % 3 = 1

++:自加1

1)在赋值语句中:

情形一:

int a = 3,b;

b = a++; //++在后,先取出a的值参与其他运算,再对a进行自加1.

情形二:

int a = 3,b;

b = ++a; //++在前,先让a自加1,再将a的值赋值给b。

2)在打印语句中:

情形一:

int a = 3;

printf("%d\n",a++); //++在后,先取出a的值输出,再对a进行自加1.

情形二:

int a = 3;

printf("%d\n",++a); //++在前,先让a自加1,再将a的值输出。

3)独立成语句:

情形一:

int a = 3;

a++;

printf("%d\n",a); //4

情形二:

int a = 3;

++a;

printf("%d\n",a); //4

--:自减1

同理。

简答题:设i=3,请分析 (++i)+(++i)+(++i)的结果?

这种行为是C语言的未定义行为,运算结果受编译器影响。

3.2、逻辑运算符

&& || !

真假性:看算式结果,非0为真,0为假。

&& 逻辑与

运算法则:

全真则真,有假则假。

|| 逻辑或

运算法则:

有真则真,全假则假。

截断法则:

&& :如果前面的表达式为假,则后面表达式不执行。

|| :如果前面的表达式为真,则后面表达式不执行。

示例1:或运算截断法则

示例2:与运算截断法则

3.3、位运算符

& | ~ ^ << >>

位运算针对的就是二进制位

& 位与

运算法则:

全1则1,有0则0。

示例:

0xab & 0x5f

1010 1011

& 0101 1111

= 0000 1011 = 0xb

| 位或

运算法则:

有1则1,全0则0。

示例:

0xab | 0x5f

1010 1011

| 0101 1111

= 1111 1111 = 0xff

~ 按位取反

运算法则;

1取反得0,0取反得1

~0xab

1010 1011

~=0101 0100 = 0x54

~1

正数原码 0000 0001

正数补码 0000 0001

~= 1111 1110

负数补码 1111 1110

负数原码 1000 0010

= -2

~(-20)

负数原码 1001 0100

负数补码 1110 1100

~= 0001 0011

正数补码 0001 0011

正数原码 0001 0011

= 19

十进制数取反:

~x => -(x+1)

^ 按位异或

运算法则:

相同为0,不同为1

示例:

0xab ^ 0x5f

1010 1011

^ 0101 1111

= 1111 0100 = 0xf4

<< 左移

运算规律:

x左移n位 => x乘2的n次方

左移几位右边补几个0

>> 右移

运算规律:

x右移n位 => x除2的n次方 (往小取整)

右移几位左边补符号位

-25 >> 2 =>

负数原码 1001 1001

负数补码 1110 0111

>>2 1111 1001

负数补码 1111 1001

负数原码 1000 0111

= -7

置位运算:

a:oxab

1、将变量a第四位置置1

a:1010 1011

|  0001 0000      -->(1<<4)

   1011 1011 

2、将变量a第五位置置0

a:1010 1011

& 1101 1111    -->~(1<<5)

   1000 1011

3.4、关系运算符

>   <   >=   <=   ==  !=

3.5、赋值运算符

=  +=    -=   *=   /=   %= ...

a+=1 => a = a + 1

a*=3 => a = a * 3

3.6、条件运算符

三目运算符 ?:

表达式1 ? 表达式2 : 表达式3

运算法则:

先判断表达式1是否成立,如果成立则取表达式2的值,否则取表达式3的值。

4、运算符优先级

优先级从高向低:

单目运算符 ! ~ ++ --

算术运算符 * / % + -

移位运算符 << >>

关系运算符 < <= > >= == !=

位与运算符 &

异或运算符 ^

位或运算符 |

逻辑运算符 && ||

条件运算符 ?:

赋值运算符 = += *= /= ...

口诀:单算移关与,异或逻条赋

结合性从右向左:单条赋

(五)内存分区

1、内存分区位置图解

高地址

-------------------------------------------------

栈区:由系统自动开辟自动释放的空间,用于存放局部变量

-------------------------------------------------

堆区:由用户手动申请手动释放的空间,malloc和free

-------------------------------------------------

全 .bss 未初始化的全局变量和静态变量

局 ----------------------------------------------

区 .data 已初始化的全局变量和静态变量

-------------------------------------------------

常量区:存放常量

-------------------------------------------------

代码段:存放用户代码

-------------------------------------------------

低地址

654ae8a1fc3e02ded75dced4c695cda7.png

2、大小端(三种方法)

概念:多字节数据在存储的过程中会有不同的存储形式。

小端模式:高地址存高位数据,低地址存低位数据。

大端模式:高地址存低位数据,低地址存高位数据。

大小端验证方法:

方法一:

#include<stdio.h>

int main(int argc, const char *argv[])

{

int num = 0x12345678;

char ch = (char)num;

printf("%#x\n",ch); //0x78 低地址存了低位数据,因此是小端存储模式。

return 0;

}

方法二:
    
          利用共用体验证
          #include<stdio.h>
          union data
          {
              char a;
              int b;
          };
          int main(int argc, const char *argv[])
          {
              union data s;
              s.b = 0x12345678;
              printf("%#x\n",s.a);
              return 0;
          }  

 方法三:                                    
     
          利用指针
          #include<stdio.h>
          int main(int argc, const char *argv[])
          {
              int num = 0x12345678;               
              void *p = &num;
              printf("%#x\n",*(char *)p);
              return 0;
          }

(六)控制语句

1、输入输出语句

1.1格式化输出语句printf

#include<stdio.h>

返回值类型 函数名(参数)

int printf(const char *format, ...);

功能:按照用户指定的格式输出数据

参数: const char *format 指定格式

%c -- 字符

%s -- 字符串

%o -- 八进制整数 %#o

%d -- 十进制整数 %ld %lld

%x -- 十六进制整数 %#x

%f -- 浮点型小数 %lf

%e -- 指数

%u -- 无符号数

%p -- 地址

... 要输出的数据

返回值:实际输出的字符个数

1.2格式化输入函数scanf

int scanf(const char *format, ...);

功能:按照用户指定的格式从终端输入数据

参数:const char *format 格式

同printf

... 要输入的数据的地址

返回值:成功输入的数据的个数

应用示例:

int num1 = 0,num2 = 0;

printf("请输入两个整数:");

scanf("%d %d",&num1,&num2);

printf("两数之和为:%d\n",num1+num2);

注意:

1.scanf中不要出现\n \r \t

scanf("%d\n",&a);

scanf("%d\t",&a);

scanf("%d\r",&a);

输入整型数后按回车并不能结束,只有再次输入一个字符才能结束。

或者结束进程:ctrl + c

2.输入字符时

char a,b;

scanf("%c%c",&a,&b); //两个字符紧挨着输入

// 若输入abc,只能成功输入字符a和字符b。

// 如果输入a空格b,那么空格会被输入给b。

3.输入整数和字符时

int a;

char b;

scanf("%d%c",&a,&b);

//输入1空格a或者1回车a,会将空格符或换行符赋给b。

scanf("%d %c",&a,&b);

//输入1空格a或者1回车a都可以成功输入。

scanf("%d,%c",&a,&b);

//输入1,a可以成功输入。

scanf("%*c%c",&b);

//%*c吞掉任意一个字符

//空格为scanf输入结束的标志

1.3 字符输出函数putchar

int putchar(int c);

功能:向终端输出一个字符

参数:要输出字符的ascii

返回值:同参数

输出换行符:

putchar('\n')

putchar(10)

1.4 字符输入函数getchar

int getchar(void);

功能:从终端输入一个字符

参数:void 无

返回值:输入字符的ascii

练习:实现大小写转换 (用getchar实现输入,用putchar输出)

如果输入的是大写字母,则输出小写字母

如果输入的是小写字母,则输出大写字母

如果输入的不是字母,则输出“输入有误”

2、分支结构语句

1. if -else

基本结构:

if(表达式)

{

语句块1;

}else

{

语句块2;

}

练习:任意输入一个年份,判断是平年还是闰年。

置闰规则:

普通闰年:公历年份是4的倍数,且不是100的倍数的,为闰年(如2004年、2020年等就是闰年)。

世纪闰年:公历年份是整百数的,必须是400的倍数才是闰年(如1900年不是闰年,2000年是闰年)。

嵌套结构:

if(表达式1)

{

if(表达式2)

{

语句块1;

}else

{

语句块2;

}

}else

{

语句块3;

}

分层结构:

if(表达式1)

{

语句块1;

}else if(表达式2)

{

语句块2;

}else

{

语句块3;

}

练习.输入三角形边长,求面积(开平方sqrt)

s = 1/2 * (a+b+c);

____________________

面积:area = √ s*(s-a)*(s-b)*(s-c)

提供开方函数如下:

#include <math.h>

double sqrt(double x);

功能:开根号

参数:double x 被开根号的值

返回值:开根号后得到的结果

编译时注意连接math库 即gcc xxx.c -lm

编程思路:

1.定义三个变量,从终端输入数据。

2.判断三边长是否能构成三角形

如果能够构成三角形

代入s = 1/2 * (a+b+c)算出s

通过调用sqrt函数对s*(s-a)*(s-b)*(s-c)开方算出面积

输出面积

如果不能构成三角形

输出“三边长无法构成三角形”

2.switch-case

基本结构:

switch(变量或表达式)

{

case 常量1 : 语句块1;break;

case 常量2 : 语句块2;break;

...

case 常量n : 语句块n;break;

default :语句块n+1;

}

示例:终端输入一个数,输出对应星期几。

练习:实现简易计算器,能进行加减乘除运算。 (用switch-case实现)

要求:输入 1 + 1

输出 2


3、循环语句

1.for

基本结构

for(表达式1;表达式2;表达式3)

{

语句块;

}

表达式1:赋初值

表达式2:循环判断条件

表达式3:增值或减值语句

执行顺序:

先执行表达式1赋初值,再判断表达式2是否成立,如果成立则执行语句块;语句块执行完毕后再执行表达式3,再次判断表达式2是否成立,成立则执行语句块,依次循环;直到表达式2不成立,则循环结束。

练习1:由用户控制循环次数,输出hello world

练习2:求100以内最大能被17整除的数

思考:如何找出所有能被17整除的数?

如何只输出最大的那个?

方法1:从小往大找

方法2:从大往小找

练习2:输出200-400之间能被3整除且个位数字为6的整数。

思考:如何利用循环限制数据范围? 200-400

如何判断这个数据能被3整除?

如何判断数据的个位为6?

嵌套结构

for(表达式1;表达式2;表达式3)

{

for(表达式4;表达式5;表达式6)

{

语句块;

}

}

外层循环执行n次,内层循环执行m次,语句块共执行n*m次

图形题:

输出以下图形

行数由终端输入

for嵌套: 外层循环控制行数,内层循环控制每行星的个数

练习:用户输入行数,打印菱形

注:菱形行数为奇数。

练习:3、4、5三个数能组成多少互不相同且无重复的数

2.while

基本结构:

while(表达式)

{

语句块;

}

执行顺序:

判断表达式是否成立,如果表达式成立则执行语句块,

执行完毕后再次判断表达式是否成立,如果成立则再次执行语句块,依次循环。

直到表达式不成立,则循环结束。

面试题:

找到所有水仙花数:水仙花数是一个三位数,个位、十位、百位的立方和等于其本身。

3.do...while

do

{

语句块;

}while(表达式);

执行顺序:

先执行语句块,再判断表达式是否成立。如果成立则再次执行语句块,依次循环。

直到表达式不成立则跳出循环。

4.goto语句

标签:

语句;

...

goto 标签;

执行规则:当遇到goto语句时,程序跳转到标签位置继续执行。

注意事项:

1.标签一般用大写字母定义。

2.goto只能在函数体内部跳转。

3.goto会破坏程序的顺序性,一般不建议使用。

死循环:

while(1) //永真循环

for(;;)

5.循环控制语句

break:跳出循环

continue:跳出本次循环,继续下次循环。

continue后面的语句不再执行。

continue用法示例:

(七)、数组

1、概念

一组同种类型的数据的集合

定义格式:

[存储类型] [数据类型] 数组名[大小];

(auto) int arr[10];

大小:元素个数 arr[0] ~ arr[9]

数组名:1) 遵守标识符的命名规则

2) 数组名即代表数组的首地址

arr <=> &arr[0]

2、数组初始化

(1)全部初始化

int num[5] = {1,2,3,4,5};

初始化完的效果;

num[0] = 1

num[1] = 2

num[2] = 3

num[3] = 4

num[4] = 5

(2)部分初始化

int num[5] = {1,2};

初始化完的效果;

num[0] = 1

num[1] = 2

num[2] = 0

num[3] = 0

num[4] = 0

数组部分初始化时,未被初始化的元素值为0

3.未初始化

int num[5];

数组未初始化,其值均为随机值。(局部)

3、数组清零

int num[5] = {0,0,0,0,0};

int num[5] = {0};

4、数组大小

元素个数 * 数据类型大小

示例:

int a[5] = {1,2}; //20字节

char b[10]; //10字节

double c[3] = {1.1,2.2,3.3}; //24字节

5、数组输入和输出

int num[5] = {0};

scanf("%d",&num[0]);

scanf("%d",&num[1]);

....

scanf("%d",&num[4]);

循环优化:

int i;

for(i=0;i<5;i++)

{

scanf("%d",&num[i]);

}

数组的遍历:

for(i=0;i<5;i++)

{

printf("num[%d] = %d\n",i,num[i]);

}


6、冒泡排序

将5 4 3 2 1从小到大排序

原数列

5 4 3 2 1

第一轮:

4 5 3 2 1

4 3 5 2 1

4 3 2 5 1

4 3 2 1 5

第二轮:

3 4 2 1 5

3 2 4 1 5

3 2 1 4 5

第三轮:

2 3 1 4 5

2 1 3 4 5

第四轮:

1 2 3 4 5

总结:

N个数 比较N-1轮

每轮比较次数从N-1递减

实现:

for循环嵌套实现,外层循环控制比较轮数,内层循环控制比较次数。

核心代码:

N个数从大到小排序:

for(i=0;i<N-1;i++)

{

for(j=0;j<N-1-i;j++)

{

if(num[j]<num[j+1])

{

tmp = num[j];

num[j] = num[j+1];

num[j+1] = tmp;

}

}

}


7、选择排序

从小到大排序

红色箭头i:锁定位置 从第一个走到倒数第二个数

蓝色箭头j:寻找最值 从箭头i指向的数据后开始一直走到最后一个数据

绿色箭头k:暂时标记 如果箭头j找到更小的就把k叫过去进行标记

排序思路:

先让i锁定一个位置,让k记录i的位置,然后j从i的下一个数据开始找较小的数,当找到之后,让k临时标记,j继续查找更小的,直达走到最后一个数据,j任务完成。

只需要将i标记的位置和k标记的位置数据进行交换,即可将最小的数放到前面。

算法实现:

N个数从小到大排序:

for(i=0;i<N-1;i++)

{

k=i;

for(j=i+1;j<N;j++)

{

if(num[j]<num[k])

{

k=j;

}

}

if(i!=k)

{

tmp=num[i];

num[i]=num[k];

num[k]=tmp;

}

}

代码实现:

8、二维数组

概念:有行有列数组

定义格式:

[存储类型] [数据类型] 二维数组名[行数][列数];

(auto) int num[2][3];

元素个数: 行数 * 列数

初始化:

1.全部初始化

int num[2][3] = {1,2,3,4,5,6};

int num[2][3] = { {1,2,3},{4,5,6}};

初始化后:

| 1 2 3 |

| 4 5 6 |

2.部分初始化

int num[2][3] = {1,2};

初始化后:

| 1 2 0 |

| 0 0 0 |

int num[2][3] = { {1},{2}};

初始化后:

| 1 0 0 |

| 2 0 0 |

3.未初始化

int num[2][3]; (局部)

//未初始化均为随机值

数组大小:

行数 * 列数 * 数据类型大小

int num[2][3]; //24字节 = 2 * 3 * 4

数组输入输出:

int num[2][3] = {0};

输入:

for(i=0;i<3;i++)

{

scanf("%d",&num[0][i]);

}

for(i=0;i<3;i++)

{

scanf("%d",&num[1][i]);

}

用循环嵌套思想优化:

for(i=0;i<2;i++)

{

for(j=0;j<3;j++)

{

scanf("%d",&num[i][j]);

}

}

输出:

for(i=0;i<2;i++)

{

for(j=0;j<3;j++)

{

printf("%d ",num[i][j]);

}

putchar(10);

}

练习:定义一个三行四列的数组,从终端录入数据,找出其最大值、最小值,并输出其下标。

例如:

max : num[1][2] = 99

min : num[0][2] = 1

练习:有一个三行三列的矩阵,如下:

| 1 2 3 |

| 4 5 6 |

| 7 8 9 |

求出其非对角线上的元素值的和。

练习:从终端录入小张、小王、小刘三位同学C语言、C++、数据结构三门课的成绩,

求出各科平均分以及三位同学的平均成绩。

9、字符数组

概念:用于存储字符串的数组称为字符数组。

char s[10] = {"hello"};

char s[10] = {'a','b','c'};

char s[10] = "hello";

char s[ ] = "hello"; //6字节

//字符串的结束标志为'\0'

char s1[3] = {"abc"}; //不合法

char s2[3] = {'a','b','c'}; //合法

//如果以字符串的形式输出,以上两种写法都没有结束标志。

字符数组输入:

1)以字符为单位输入

char str[10] = {0},i;

for(i=0;i<5;i++)

{

scanf("%c",&str[i]);

}

//输入字符个数由循环次数决定,输入不够灵活。

2)以字符串的形式输入

scanf("%s",str);

//空格无法输入

解决方案:

利用scanf("%[^\n]",str); 可以读取一行字符串,直到遇到换行符\n结束

%[]的格式控制法:%[scanfset]

以^字符开头的scanfset,表示在读入字符串时将匹配所有不在scanfset中出现的字符,遇到scanfset中的字符输入就结束。

3)字符串输入函数

char *gets(char *s);

功能:从终端获取一串字符

参数:char *s 目标字符数组的首地址

返回值:同参数

//注:gets函数不检查数组大小,容易造成越界赋值导致的栈溢出问题,因此编译时会 有危险警告。

后续可使用fgets录入字符串。

字符数组输出:

1)以字符为单位输出

char str[10] = "hello";

for(i=0;i<5;i++)

{

printf("%c",str[i]);

}

2)以字符串的形式输出

printf("%s\n",str);

//以‘\0’作为输出结束标志

3)字符串输出函数

int puts(const char *s);

功能:向终端输出一串字符

参数:const char *s 要输出的字符串的首地址

返回值:执行成功 - 非负数

今日作业:

1)打印杨辉三角的前十五行 (已知第一列为1)

1

1 1

1 2 1

1 3 3 1

1 4 6 4 1

....

#include <stdio.h>
int main(int argc, const char *argv[])
{
int a[15][15]={
      
      {0}};
int i,j;
for(i=0;i<15;i++)
{
a[i][0]=1;
for(j=1;j<=i;j++)
a[i][j]=a[i-1][j-1]+a[i-1][j];
for(i=0;i<15;i++){
for(j=0; j<=i;j++)
printf("%8d",a[i][j]);
printf("\n");
}
return 0;

2)从终端录入一串字符串,统计其中数字字符的个数,将其中数字字符求和输出。

#include <stdio.h>
int main(int argc, const char *argv[]){
char s[100];
int i=0;
int j=0;
int sum=0 ;
gets(s);
/ /puts(s);
while(s[i]!='\0'){
if('0'<=s[i]&&s[i]<='9'){
j++;
sum+=(s[i]-'0');
i++ ;
}
printf("字符串中数字字符的个数为: %d\n", j);
printf("字符串中数字字符求和为: %d\n",sum);
return 0;

10、string家族函数


    #include <string.h>
    strlen
    函数原型:size_t strlen(const char *s);
    功能:计算字符串的实际长度 (截止到‘\0’)
    参数:const char *s 字符串的首地址
    返回值:size_t 字符串实际长度
    
    面试题:strlen和sizeof的区别?
    答:strlen是个函数,计算的字符串的实际长度(截止到‘\0’)。
        而sizeof是个关键字,计算的是变量或者类型占用的内存的大小。    
        char str[10] = "hello";
        sizeof(str) = 10;
        strlen(str) = 5;
    
    练习:仿写strlen功能。
        从终端任意输入一串字符,输出其实际长度。
    
    strcpy
    char *strcpy(char *dest, const char *src);
    功能:实现字符串复制 (包含'\0')
    参数:char *dest   目标字符串地址
          const char *src 源字符串地址
    返回值: dest 目标字符串地址
    char *strncpy(char *dest, const char *src, size_t n)
    //按照指定的字符个数实现字符串复制
    思考题:char s1[32] = "hello",如何将“world”存储到s1中?
        s1 = "world" ;  //错误,因为s1是个地址常量,常量不能被赋值。
        s1[0] = 'w';
        s1[1] = 'o';
        ...        
        //可以,但没必要
        用strcpy来实现:
        strcpy(s1,"world");
        
    strcat
    char *strcat(char *dest, const char *src);
    功能:实现字符串拼接 (包含'\0')
    参数:char *dest   目标字符串地址
          const char *src 源字符串地址
    返回值: dest 目标字符串地址    
    char *strncat(char *dest, const char *src, size_t n)
    //按照指定字符个数进行拼接
    
    strcmp
    int strcmp(const char *s1, const char *s2);
    功能:比较首个不相同的字符的ascii(包含'\0')
    参数:要比较的两个字符串的首地址
    返回值:  s1 > s2   1
              s1 = s2   0
              s1 < s2   -1
    int strncmp(const char *s1, const char *s2, size_t n);
    //按照指定字符个数进行比较

(八)指针

1、概念


      地址:内存中每个字节都有一个编号,这个编号就是地址(一般用十六进制表示)
      指针:指针就是地址,地址是个常量。
      指针变量:用于存放指针(地址)的变量称为指针变量 

2、定义格式


      [存储类型]  [数据类型]   *指针变量名;
        (auto)     int         *p;
       // 在定义指针,*起标识作用。
       //定义指针时,数据类型表示的指向的地址存储的数据类型。

3、指针的运算


      1)赋值运算
         a.指针指向普通变量
           int num = 10;
           1 - int *p = &num;  //指针p指向了num这个整型变量。
           2 - int *p = NULL;
               p = &num;
         b.指针指向一维数组
           int num[5] = {1,2,3,4,5};
           1 - int *p = num;     //数组名就代表数组首个元素的地址
           2 - int *p = &num[0]; //指针p指向了数组首地址
           p <=> num
           访问元素num[i]的值:
              通过数组名直接访问:  num[i]  *(num+i)
              通过指针名间接访问:  p[i]    *(p+i)
           访问元素num[i]的地址:
              通过数组名直接访问:  &num[i]   num+i
              通过指针名间接访问:     &p[i]      p+i     
           
           推导过程: 指针p指向数组num,p里面保存了num数组的首地址,即p指向数组首个元素的地址。
                      p => &num[0]  那p+i则为num[i]的地址 &num[i]
                     *(p+i)相当于对&num[i]进行取值运算,因此*(p+i) => num[i]                      
              
            约定地址的等级高于元素值的等级
           
            升级 : 加&   去掉[]            
            降级 :加*   添加[]
              
         c.指针指向另一个指针指向的数据        
           int num;
           int *p = &num;
           int *q = p;
          
      2)取值取址运算
         &  取地址符 :取变量的地址
         *  取值符   :取地址上的值
         &和*互为逆运算,可以抵消。    
            *&num = num
    
      3)算术运算
        p+1 :向高地址方向移动一个数据的大小
        p-1    :向低地址方向移动一个数据的大小    
        //以上两种运算,p的指向不变
        p++、p--才会真正改变指针指向
        
      4)关系运算
         >   <   >=  <=  ==  !=
         指针之间的关系运算比较的地址的高低,
         指向高地址的指针大于指向低地址的指针。
         指向不同区域、不同类型的指针都没有关系运算意义。

4、指针大小


      在32位操作系统中,指针的大小恒为4字节。
      指针的有效寻址空间就是4G。
      寻址范围:0 ~ 2^32-1     
      4,294,967,296字节 = 4,194,304KB = 4096MB = 4GB
      int num;
      int *p1 = &num; 
      char ch;
      char *p2 = &ch;
      double f;
      double *p3 = &f;

       sizeof(p1) sizeof(p2)  sizeof(p3)
         
      练习:1.终端录入一串字符串,将其中字符a替换成字符*输出
              (用指针实现)         
            2.通过指针实现字符串倒置
               字符从终端输入
               输入 hello
               输出 olleh
               
        段错误Segmentation fault (core dumped)
        原因:1)内存访问越界
             2)使用了野指针
                野指针:没有明确的指向对象
                定义指针时如果没有明确指向则初始化为空,防止出现野指针问题。

5、指针的修饰(const、void)


      const 常量化
        作用:只读修饰
        1)const修饰普通变量
         #include<stdio.h>
         int main(int argc, const char *argv[])
         {
             const int num = 10;
             //num = 20;   // error: 只读变量num无法被赋值
      
             int *p = &num;
             *p = 20;  
             printf("%d\n",num);
             return 0;
         }
        
        2)const修饰指针
           int num1 = 10,num2 = 20;
           const int *p  = &num1;
           //int const *p = &num1;
           *p = 20;  //错误,const修饰*p整体,无法通过更改*p来更改指针指向的地址上的值
           p = &num2; //可行,指针指向可变。
        
           int num1 = 10,num2 = 20;
           int * const p  = &num1;
           *p = 20;  //可行,*p可更改。
           p = &num2; //错误,因为const修饰p,则指针指向不可更改。   
             
       void    空类型
          void *p = NULL; //空类型指针指向数据类型是不确定的。
          //空类型指针可以指向任何一种类型的变量。
          int a = 10;
          char b = '!';
          float c = 3.14;
          void *p = NULL;
          //空类型指针在使用时需要针对指向的数据类型进行强制类型转换。
          p = &a;
          printf("%d\n",*(int *)p);  
          p = &b;
          printf("%c\n",*(char *)p);
          p = &c;
          printf("%f\n",*(float *)p);
          

6、二级指针


    概念:指向一级指针的指针称为二级指针
    示例:
        int num = 10;
        int *p = &num;
        int **q = &p;  //二级指针q指向了一级指针p
        
        访问num的值:
           num   *p    **q
        访问num的地址
           &num   p     *q        

7、指针与数组


    直接访问:通过数组名进行访问
    间接访问:通过指向数组的指针进行访问

    1.指针与一维数组
      int num[5]  = {1,2,3,4,5};
      int *p = num;
      访问num[i]的值:
      直接访问: num[i]   *(num+i)
      间接访问: p[i]     *(p+i)
      访问num[i]的地址:
      直接访问: &num[i]   num+i
      间接访问: &p[i]     p+i    
      
      printf("%p\n",num+1);  //打印num[0]的地址
      printf("%p\n",p+1);    //打印num[0]的地址,指针指向不变。    
      
      printf("%p\n",num++);  //报错,num是数组首地址是个地址常量,常量不能自加。
      printf("%p\n",p++);    //打印num[0]的地址,再让指针向后移动。     
    
      注意:
        1)*和++都是单目运算符,优先级相同。
        2)单目运算符结合性是从右向左。
        
      *p++  :先取出p的值参与取值(*)运算,再让指针向高地址方向移动一个数据。
      *(p++):先取出p的值参与取值(*)运算,再让指针向高地址方向移动一个数据。
      
      *++p  :先让指针向高地址方向移动一个数据,再对p的值进行取值运算。
      *(++p):先让指针向高地址方向移动一个数据,再对p的值进行取值运算。
      
      ++*p  :先对p进行取值运算,再对p指向的地址上的值进行自加运算。
      ++(*p):先对p进行取值运算,再对p指向的地址上的值进行自加运算。
      
      (*p)++:先对p进行取值运算,参与完其他运算之后,再对指针指向的地址上值进行自加运算。
      
    2.指针与二维数组      
      int num[2][3];
      int *p = num;   //错误,因为二维数组的数组名是个二级地址,因此不能用一级指针直接指向。
      推导过程:
       num =>  &num[0]  =>  &&num[0][0]
       
      访问num[i][j]的元素值:
         num[i][j]   *(num[i]+j)   *(*(num+i)+j)
      访问num[i][j]的元素地址:
         &num[i][j]   num[i]+j      *(num+i)+j       

8、数组内存移动问题


    一维数组:
        &num       整个数组
        num  &num[0]   元素        
        
        int num[5];
        &num +1     20字节  
        num +1       4字节
        &num[0] +1   4字节     
     
    二维数组:    
        &num                整个数组
        num    &num[0]       行地址
        num[0] +1            列地址
        &num[i][j]          元素地址        
        
        int num[2][3];
        &num +1                 24字节
        num+1     &num[0]+1     12字节
        num[0]+1  &num[0][0]+1   4字节

9、数组指针


    概念:本质上是个二级指针,专用于指向数组(二维)的指针称为数组指针。
    定义格式:     
        [存储类型]   [数据类型]    (*数组指针名)[列数];
          (auto)       int        (*p)[i];  
        //数组指针在定义时,[]填的指向的二维数组的列数。
    示例:  
        int num[2][3];
        int (*p)[3] = num;
        
        p    行地址
        *p   列地址   元素地址
        **p  元素值        
      
        数组指针p指向了二维数组num,因此num和p等价,此时p代表的是num数组的行地址。
        访问num[i][j]的值:
        直接访问:  num[i][j]   *(num[i]+j)   *(*(num+i)+j)
        间接访问:  p[i][j]     *(p[i]+j)     *(*(p+i)+j)  
        
        访问num[i][j]的地址
        直接访问: &num[i][j]   num[i]+j      *(num+i)+j     
        间接访问: &p[i][j]       p[i]+j      *(p+i)+j

10、指针数组
       


    概念:本质上是个数组,用于存放指针(地址)的数组称为指针数组    
    定义格式:
          [存储类型]   [数据类型]   *指针数组名[大小];    
            (auto)      int         *num[i]; 
          大小:元素个数
    应用示例:
        1.指针数组存储普通变量的地址    
          int a = 10,b = 20,c = 30;
          int *p[3] = {&a,&b,&c};
          通过数组名p访问b的值:
              *p[1]     *(*(p+1))
          通过数组名p访问b的地址:
               p[1]      *(p+1)
               
        2.指针数组存储二维数组的每行首个元素的地址(列地址)       
          int  num[2][3];
          int *p[2] = {num[0],num[1]};       
            int *p[2] = {&num[0][0],&num[1][0]};
          推导过程:
          p  =>  &p[0]  =>  &num[0]  =>  num
          因此p和num等价
          
          通过数组名p访问二维数组中元素num[i][j]的值:
              p[i][j]         *(p[i]+j)       *(*(p+i)+j)
          
          通过数组名p访问二维数组中元素num[i][j]的地址:
              &p[i][j]          p[i]+j          *(p+i)+j
        
        3.指针数组存储字符串
          char *p[3] = {"hqyj","helloworld","23032!!!"};
          
          打印字符串"helloworld"
          printf("%s\n",p[1]);
          printf("%s\n",*(p+1));
          
          打印字符串“hellowolrd”中的字符‘w’
          printf("%c",p[1][5]);
          printf("%c",*(p[1]+5));
          printf("%c",*(*(p+1)+5));
        
        4.命令行传参
        argv就是一个指针数组,里面存放的是命令行传递的字符串。
        argc表示argv指针数组内存储数据的个数,即命令行传递字符串的个数。
        int main(int argc, const char *argv[])
        {
            printf("ip:%s\n",argv[1]);
            return 0;                           
        } 
        
    数组大小:
        元素个数  *  4(指针大小)
        //因为32位操作系统中指针的大小恒为4字节        
          
          


(九)函数

1、概念

具备特定功能的代码模块

优点:1)减少代码重复

2)模块化编程

2、格式

返回值类型 函数名( 参数 )

int main (int argc, const char *argv[])

{

...

//代码块

return 0; //返回值

}

3、分类

1)有参有返回值

2)有参无返回值

3)无参无返回值

4、函数调用

1)调用有参有返回值函数

a.根据函数参数要求的数据类型进行传参

b.定义一个与函数返回值类型相同的变量来接收函数返回值

示例:

int x = 3,y = 5,res;

res = add(x,y);

printf("%d\n",res);

2)调用有参无返回值函数

根据函数参数要求的数据类型进行传参

示例:

sub(x,y);

3)调用无参无返回值函数

直接调用函数名即可。

示例:

func();

5、函数声明

将函数定义放到主函数和头文件之间即可。

示例:

#include<stdio.h>

int add(int a,int b);

void sub(int a,int b);

void func();

int main()

{

...

}

练习:仿照strcat功能,编写mystrcat函数。

//有参有返回值写法

//有参无返回值写法

练习:写一个函数swap实现两个数交换

//写法一:

传递实参的值,只是交换了形参的值,并未影响到实参本身,没有实现真正的交换。

//写法二:

传递实参的地址,通过形参访问实参地址,实现交换地址上的值,真正实现了数据交换。

函数传参:

1)值传递:相当于将实参的值进行拷贝传递给形参,对实参本身并无影响。

2)地址传递:相当于将实参的地址传递给形参,操作形参等同于操作实参。

3)数组传递:相当于传递数组首地址,操作形参相当于操作数组本身。本质就是地址传递。

6、递归函数

1.概念:自己调用自己的函数

注意:递归函数一定要设置结束条件。

2.递推阶段:从原问题出发,按照递推公式从未知到已知,最终达到终止条件。

回归阶段:按照递归终止条件求出结果,再将结果逆向代入递归公式,最终回到原问题求解。

示例:

练习:利用递归函数求斐波那契数列第20项的值

已知前两项为1.

分析:

第五项?

=第三项 + 第四项

=(第一项+第二项) + (第二项 + 第三项)

= 1 + 1 + 1 +(第一项+第二项)

= 1 + 1 + 1 + 1 + 1

那么第n项 = 第n-1项 + 第n-2项

不断拆解直到达到已知条件,再带入原问题求得最终解。

代码实现:

(十)结构体

1、概念

结构体是由用户自定义的一种构造类型,用于描述复杂事物,表示多种不同类型的数据的集合。

2、格式

struct 结构体名

{

数据类型1 成员1;

数据类型2 成员2;

....

数据类型n 成员n;

};

示例:

struct student

{

int id; //学号

char name[32]; //姓名

float score; //成绩

};

3、结构体大小

1)字节对齐规则:在结构体所有成员中,选择数据类型最大成员跟value(4字节),按照小的为单位开辟内存空间。

2)节省空间原则:在保证数据不被破坏的前提下,尽可能向上压缩以节省存储空间。

//为什么要进行字节对齐? 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。在我们之前写程序的时候可能会发现有的时候你定义的变量是一个字节,但是他在内存中依然按照四个字节给你存储,因为在32为系统中,4字节对齐执行效率是最快的。这就是一种牺牲内存换取性能的方案。

特殊情况:

struct num

{

char a; //4

short b; char和short在四字节内部2字节对齐。

char c; //4

int d; //4

};

sizeof(struct num) = 12

4、结构体变量

1.概念:通过结构体类型定义的变量称为结构体变量。

2.定义格式

1)先定义结构体类型,再通过结构体类型定义结构体变量。

struct phone

{

int ID;

char Brand[10];

char Model[20];

char CPU[20];

float Price;

};

struct phone p1;

2)定义结构体类型的同时定义结构体变量。

struct phone

{

int ID;

char Brand[10];

char Model[20];

char CPU[20];

float Price;

}p1,p2;

struct phone p3;

3)在结构体嵌套时,可以缺省结构体名定义结构体变量。

struct student

{

int ID;

char Name[32];

struct {

float chinese;

float math;

char PE;int English;

} cj;

};

3.结构体变量初始化

1)定义结构体变量的同时进行初始化

struct phone

{

int ID;

char Brand[10];

char Model[20];

char CPU[20];

float Price;

}p2 = {1002,"小米","13","骁龙8Gen1",3999};

struct phone p1 = {1001,"华为","P60","骁龙8Gne2",5999};

2)定义结构体变量时未初始化,之后再分别对成员赋值。

结构体变量通过 . 访问成员 : 结构体变量名.成员

示例:

struct phone p3; //未初始化

strcpy(p3.Brand,"APPLE");

strcpy(p3.Model,"14ProMax");

strcpy(p3.CPU,"A16");

p3.Price = 8999;

补充:
    用typedef给结构体类型起别名。
      typedef struct 
      {
          int   ID;
          char  Brand[10];
          char  Model[20];
          char  CPU[20];
          float Price;
      }PH;
      //ph是结构体类型  
      PH  p1;  //p1是结构体变量

5、结构体数组


    1.概念:用于存放结构体变量的数组,结构体数组中存放的元素都是结构体类型的。
    2.定义格式:
        先定义结构体类型,再通过结构体类型定义结构体数组。
        typedef struct 
        {
            int   ID;
            char  Brand[10];
            char  Model[20];
            char  CPU[20];
            float Price;
        }PH;
        PH  ph_arr[20];
    3.结构体数组初始化
      PH ph_arr[10] ={
                       {1001,"华为","P60","骁龙8Gen2",5999},
                       {1002,"小米","13","骁龙8Gen1",3999}                       
                      };

    4.结构体数组的输入输出
      struct phone ph_arr[5] = {0};
      scanf("%d",&ph_arr[0].id);
      通过循环进行优化:
      for(int i=0;i<5;i++)
      {
          scanf("%d %s %s %s %f",&ph_arr[i].id,ph_arr[i].brand,ph_arr[i].mod,ph_arr[i].cpu,&ph_arr[i].price);
      }      
      for(int i=0;i<5;i++)
      {
          printf("%d %s %s %s %f",ph_arr[i].id,ph_arr[i].brand,ph_arr[i].mod,ph_arr[i].cpu,ph_arr[i].price);
      }

6、结构体指针


    概念:结构体类型的指针称为结构体指针
    定义格式:
        struct 结构体名 *结构体指针名;
    应用示例:    
        struct phone 
        {
            int   ID;
            char  Brand[10];
            char  Model[20];
            char  CPU[20];
            float Price;
        }arr[10];
        struct phone *p = arr;
     访问形式:
       给数组中下标为1的元素的ID成员赋值:
       (*(p+1)).ID = 1001;
        (p+1)->ID = 1001;

7、共用体


    概念:不同数据类型的数据使用共同的存储区域,这种数据构造类型称为共用体,又称联合体。
    定义格式: 
         union 共用体名
         {
            成员列表;
         };
     例如:
         union data
         {
           char  cval;
           int   ival;
           float fval;
         };
    共用体类型变量定义:
       (1)先定义共用体类型,后定义变量。
          union data
         {
           char  cval;
           int   ival;
           float fval;
         };
         union data a,b,c;
       (2)定义共用体类型的同时定义变量。
        union data
         {
           char  cval;
           int   ival;
           float fval;
         }a,b,c;
         如果不再定义新的变量,也可以将共用体的名字省略:
         union
         {
           char  cval;
           int   ival;
           float fval;
         }a,b,c;
         
    共用体大小计算:
        结构体占用内存大于等于所有成员占用内存的总和,因为成员之间可能存在空隙,而共用体占用的内存空间,等于最大的成员变量占用的内存。
        共用体采用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,会覆盖掉原有的成员数据。
        比如 :共用体data里占用内存最大的是int类型数据,占用4个字节,那么共用体data占用的内存大小为4。
          
        union test
        {
          char s[20];
          int b;
          double c;
        }a;
        sizeof(a) = 20;
    
 

8、枚举


    定义:只能取预先定义值的数据类型是枚举类型。
    在实际编程中,有些数据的取值往往是有限的,只能是非常少量的整数,并且最好为每个值都取一个名字,以方便在后续代码中使用,比如一个星期只有七天,一年只有十二个月,一个班每周有六门课程等。
    
        格式: 
         enum 枚举类型名
         {
           枚举元素列表
         };
        例如:  列出一个星期有几天:
              enum week
              { 
                 Mon, 
                 Tues, 
                 Wed, 
                 Thurs, 
                 Fri, 
                 Sat, 
                 Sun 
              };
              可以看到,我们仅仅给出了名字,却没有给出名字对应的值,
              因为枚举值默认从0开始,往后逐个加1,也就是说,week 中的 Mon、Tues ...... Sun
              对应的值分别为 0、1 ...... 6。
              
              我们也可以给每个名字都指定一个值:
              enum week
              { 
                Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 
              };
              由于枚举类型值逐个递增1,我们也可以只给第一个名字指定一个值:
              enum week
              { 
                Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun 
              };
    说明:   1)枚举类型是一种类型,通过它可以定义枚举变量
               enum week a,b,c;
            2)可以将列表里的值赋给枚举变量
               enum week a = Mon, b = Web, c = Sat;
            3)枚举列表中的Mon、Tues、Wed这些标识符的作用范围是全局的(严格来说是 main() 函数内部)
            不能再定义与他们名字相同的变量。
            4)Mon、Tues、Wed等都是常量,不能再次赋值,只能将他们赋给其他变量。
    示例:判断用户输入的是星期几。
            #include <stdio.h>
             int main(){
             enum week{ 
               Mon = 1, 
               Tues, 
               Wed, 
               Thurs, 
               Fri, 
               Sat, 
               Sun 
             } day;
             
             scanf("%d", &day);
             switch(day){
                case Mon: puts("Monday"); break;
                case Tues: puts("Tuesday"); break;
                case Wed: puts("Wednesday"); break;
                case Thurs: puts("Thursday"); break;
                case Fri: puts("Friday"); break;
                case Sat: puts("Saturday"); break;
                case Sun: puts("Sunday"); break;
                default: puts("Error!");
              }    
              return 0;
            }               
           

猜你喜欢

转载自blog.csdn.net/qq_52049228/article/details/129935370