C语言数据结构以及原理
变量(variable)
它有三个重要属性:名称
、值
、类型
。
变量就是可以变化的量,而且每个变量都有会一个名字(标识符)变量占据内存中一定的存储单元。使用变量之前必须先定义变量,要区分变量名和变量值是两个不同的概念。
变量在使用之前应该被初始化。未初始化的变量的值是未定义的,可能包含任意的垃圾值。因此,为了避免不确定的行为和错误,建议在使用变量之前进行初始化。
在 C 语言中,如果变量没有显式初始化,那么它的默认值将取决于该变量的类型和其所在的作用域。
变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。
C 语言也允许定义各种其他类型的变量,比如枚举、指针、数组、结构、共用体等等。
变量的命名
命名要求:
- 只能由字母(包括大写和小写)、数字和下划线( _ )组成。
- 不能以数字开头。
- 长度不能超过63个字符。
- 变量名中区分大小写的。
- 变量名不能使用关键字。比如:char float
变量的分类
局部变量:位于大括号里的
全局变量:在整个项目里的
注意:
局部变量
可以与全局变量
同名
当局部变量
和全局变量
同名的时候,局部变量
会被优先使用。就实现了变量覆盖。
局部变量
在函数中定义的变量,它的作用域在函数中。
在复合语句中定义的变量,他在复合语句有效。
有参函数中形参也是局部变量。
全局变量
作用范围:
从定义全局变量的位置起到该源程序结束为止
1.全局变量和局部变量同名,就近原则,谁近用谁。
从变量作用域分 局部/全局变量
从变量值存在时间长短分 动态/静态存储变量
程序区、静态存储区、动态存储区。
其中全局变量是静态变量
变量内存分析
-
内存模型
-
内存模型是
线性
的(有序的) -
对于 32 机而言,最大的内存地址是2^32次方bit(4294967296)(4GB)
-
对于 64 机而言,最大的内存地址是2^64次方bit(18446744073709552000)(171亿GB)
-
CPU 读写内存
-
CPU 在运作时要明确三件事
-
存储单元的地址(
地址信息
) -
器件的选择,读 or 写 (
控制信息
) -
读写的数据 (
数据信息
)
-
-
如何明确这三件事情
- 通过地址总线找到存储单元的地址
- 通过控制总线发送内存读写指令
- 通过数据总线传输需要读写的数据
地址总线: 地址总线宽度决定了CPU可以访问的物理地址空间(寻址能力)
- 例如: 地址总线的宽度是1位, 那么表示可以访问 0 和 1的内存
- 例如: 地址总线的位数是2位, 那么表示可以访问 00、01、10、11的内存
数据总线: 数据总线的位数决定CPU单次通信能交换的信息数量
- 例如: 数据总线:的宽度是1位, 那么一次可以传输1位二进制数据
- 例如: 地址总线的位数是2位,那么一次可以传输2位二进制数据
控制总线: 用来传送各种控制信号
-
写入流程
- CPU 通过地址线将找到地址为 FFFFFFFB 的内存(找内存)
- CPU 通过控制线发出内存写入命令,选中存储器芯片,并通知它,要其写入数据。(发出指令)
- CPU 通过数据线将数据 8 送入内存 FFFFFFFB 单元中(写入数据)
-
读取流程
-
CPU 通过地址线将找到地址为 FFFFFFFB 的内存(找到内存)
-
CPU 通过控制线发出内存读取命令,选中存储器芯片,并通知它,将要从中读取数据(发出指令)
-
存储器将 FFFFFFFB 号单元中的数据 8 通过数据线送入 CPU寄存器中(读出数据)
-
变量的存储原则
-
先分配字节地址
大内存
,然后分配字节地址小的内存(内存寻址是由大到小
) -
变量的首地址,是变量所占存储空间字节地址(最小的那个地址 )
-
低位保存在低地址字节上,高位保存在高地址字节上
10的二进制: 0b00000000 00000000 00000000 00001010 高字节← →低字节
常量
什么是常量?
- "量"表示数据。常量,则表示一些固定的数据,也就是不能改变的数据
常量的类型
-
整型常量
- 十进制整数。例如:666,-120, 0
- 八进制整数,八进制形式的常量都以0开头。例如:0123,也就是十进制的83;-011,也就是十进 制的-9
- 十六进制整数,十六进制的常量都是以0x开头。例如:0x123,也就是十进制的291
- 二进制整数,逢二进一 0b开头。例如: 0b0010,也就是十进制的2
-
实型常量
- 小数形式
- 单精度小数:以字母f或字母F结尾。例如:0.0f、1.01f
- 双精度小数:十进制小数形式。例如:3.14、 6.66
- 默认就是双精度
- 可以没有整数位只有小数位。例如: .3、 .6f
- 指数形式
- 以幂的形式表示, 以字母e或字母E后跟一个10为底的幂数
- 上过初中的都应该知道科学计数法吧,指数形式的常量就是科学计数法的另一种表 示,比如123000,用科学计数法表示为1.23×10的5次方
- 用C语言表示就是1.23e5或1.23E5
- 字母e或字母E后面的指数必须为整数
- 字母e或字母E前后必须要有数字
- 字母e或字母E前后不能有空格
-
字符常量
- 字符型常量都是用’’(单引号)括起来的。例如:‘a’、‘b’、‘c’
- 字符常量的单引号中只能有一个字符
- 特殊情况: 如果是转义字符,单引号中可以有两个字符。例如:’\n’、’\t’
-
字符串常量
- 字符型常量都是用""(双引号)括起来的。例如:“a”、“abc”、“lnj”
- 系统会自动在字符串常量的末尾加一个字符’\0’作为字符串结束标志
-
自定义常量
-
-
常量类型练习
常量就是不可改变的量
C语言的常量可以分为直接常量, #define 定义的标识符常量,const 修饰的常变量,枚举常量。
-
直接常量也称为字面量,是可以直接拿来使用,无需说明的量,比如:
- 整型常量:13、0、-13;
- 实型常量:13.33、-24.4;
- 字符常量:‘a’、‘M’
- 字符串常量:”I love imooc!”
宏定义:
#define //定义的值是常量
空宏定义:
#define S345 是空宏定义
一般空的宏定义的作用是
- 对函数进行标识、说明
- 可以解决编译器兼容的问题,
例题
-
在C语言中,可以用一个标识符来表示一个常量,称之为符号常量。 符号常量在使用之前必须先定义,其一般形式为:
#define 标识符 常量值
#include <stdio.h>
#define POCKETMONEY 10 //定义常量及常量值
int main()
{
// POCKETMONEY = 12; //小明私自增加零花钱对吗?
printf("小明今天又得到%d元零花钱\n", POCKETMONEY);
return 0;
}
const 修饰的常变量: const是不变的的意思,在语法层面对变量进行修饰告诉编译器这个变量的值不能改变。
枚举常量:一般用于可以一一例举的如血型,性别,星期数;
~枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量。,它可以让数据更简洁,更易读。~
离散值就是孤立的点集,像区间,它在每一点上都是连续的,而像整数集,它的每一元素之间都有一点的距离。
整数类型
下表列出了关于标准整数类型的存储大小和值范围的细节:
类型 | 存储大小 | 值范围 |
---|---|---|
char | 1 字节 | -128 到 127 或 0 到 255 |
unsigned char | 1 字节 | 0 到 255 |
signed char | 1 字节 | -128 到 127 |
int | 2 或 4 字节 | -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647 |
unsigned int | 2 或 4 字节 | 0 到 65,535 或 0 到 4,294,967,295 |
short | 2 字节 | -32,768 到 32,767 |
unsigned short | 2 字节 | 0 到 65,535 |
long | 4 字节 | -2,147,483,648 到 2,147,483,647 |
unsigned long | 4 字节 | 0 到 4,294,967,295 |
注意,各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。
以下列出了32位系统与64位系统的存储大小的差别(windows 相同):
浮点类型
下表列出了关于标准浮点类型的存储大小、值范围和精度的细节:
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
float | 4 字节 | 1.2E-38 到 3.4E+38 | 6 位有效位 |
double | 8 字节 | 2.3E-308 到 1.7E+308 | 15 位有效位 |
long double | 16 字节 | 3.4E-4932 到 1.1E+4932 | 19 位有效位 |
头文件 float.h 定义了宏,在程序中可以使用这些值和其他有关实数二进制表示的细节。
void 类型
void 类型指定没有可用的值。它通常用于以下三种情况下:
序号 | 类型与描述 |
---|---|
1 | 函数返回为空 C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status); |
2 | 函数参数为空 C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void); |
3 | 指针指向 void 类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。 |
数组
C数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。
结构体中的数据成员可以是基本数据类型(如 int、float、char 等),也可以是其他结构体类型、指针类型等。
二维数组与函数
- 值传递
#include <stdio.h>
// 和一位数组一样, 只看形参是基本类型还是数组类型
// 如果是基本类型在函数中修改形参不会影响实参
void change(char ch){
ch = 'n';
}
int main()
{
char cs[2][3] = {
{'a', 'b', 'c'},
{'d', 'e', 'f'}
};
printf("cs[0][0] = %c\n", cs[0][0]); // a
change(cs[0][0]);
printf("cs[0][0] = %c\n", cs[0][0]); // a
return 0;
}
- 地址传递
#include <stdio.h>
// 和一位数组一样, 只看形参是基本类型还是数组类型
// 如果是数组类型在函数中修改形参会影响实参
void change(char ch[]){
ch[0] = 'n';
}
int main()
{
char cs[2][3] = {
{'a', 'b', 'c'},
{'d', 'e', 'f'}
};
printf("cs[0][0] = %c\n", cs[0][0]); // a
change(cs[0]);
printf("cs[0][0] = %c\n", cs[0][0]); // n
return 0;
}
#include <stdio.h>
// 和一位数组一样, 只看形参是基本类型还是数组类型
// 如果是数组类型在函数中修改形参会影响实参
void change(char ch[][3]){
ch[0][0] = 'n';
}
int main()
{
char cs[2][3] = {
{'a', 'b', 'c'},
{'d', 'e', 'f'}
};
printf("cs[0][0] = %c\n", cs[0][0]); // a
change(cs);
printf("cs[0][0] = %c\n", cs[0][0]); // n
return 0;
}
- 形参错误写法
void test(char cs[2][]) // 错误写法
{
printf("我被执行了\n");
}
void test(char cs[2][3]) // 正确写法
{
printf("我被执行了\n");
}
void test(char cs[][3]) // 正确写法
{
printf("我被执行了\n");
}
- 二维数组作为函数参数,在被调函数中不能获得其有多少行,需要通过参数传入
void test(char cs[2][3])
{
int row = sizeof(cs); // 输出4或8
printf("row = %zu\n", row);
}
- 二维数组作为函数参数,在被调函数中可以计算出二维数组有多少列
void test(char cs[2][3])
{
size_t col = sizeof(cs[0]); // 输出3
printf("col = %zd\n", col);
}
二维数组
-
所谓二维数组就是一个一维数组的每个元素又被声明为一 维数组,从而构成二维数组. 可以说二维数组是特殊的一维数组。
-
示例:
-
int a[2][3] = { {80,75,92}, {61,65,71}};
-
可以看作由一维数组a[0]和一维数组a[1]组成,这两个一维数组都包含了3个int类型的元素
二维数组的定义
-
格式:
-
数据类型 数组名[一维数组的个数][一维数组的元素个数]
-
其中"一维数组的个数"表示当前二维数组中包含多少个一维数组
-
其中"一维数组的元素个数"表示当前前二维数组中每个一维数组元素的个数
二维数组的初始化
-
二维数的初始化可分为两种:
-
定义的同时初始化
-
先定义后初始化
-
定义的同时初始化
int a[2][3]={ {80,75,92}, {61,65,71}};
- 先定义后初始化
int a[2][3];
a[0][0] = 80;
a[0][1] = 75;
a[0][2] = 92;
a[1][0] = 61;
a[1][1] = 65;
a[1][2] = 71;
- 按行分段赋值
int a[2][3]={ {80,75,92}, {61,65,71}};
- 按行连续赋值
int a[2][3]={ 80,75,92,61,65,71};
-
其它写法
-
完全初始化,可以省略第一维的长度
int a[][3]={{1,2,3},{4,5,6}};int a[][3]={1,2,3,4,5,6};
- 部分初始化,可以省略第一维的长度
int a[][3]={{1},{4,5}};int a[][3]={1,2,3,4};
- 注意: 有些人可能想不明白,为什么可以省略行数,但不可以省略列数。也有人可能会问,可不可以只指定行数,但是省略列数?其实这个问题很简单,如果我们这样写:
int a[2][] = {1, 2, 3, 4, 5, 6}; // 错误写法
大家都知道,二维数组会先存放第1行的元素,由于不确定列数,也就是不确定第1行要存放多少个元素,所以这里会产生很多种情况,可能1、2是属于第1行的,也可能1、2、3、4是第一行的,甚至1、2、3、4、5、6全部都是属于第1行的
- 指定元素的初始化
int a[2][3]={[1][2]=10};int a[2][3]={[1]={1,2,3}}
二维数组的应用场景
什么是数组?可以把数组看作是一行连续的多个存储单元。用更正式的说法是,数组是同类型数据元素的有序序列。
二维数组的遍历
-
二维数组a[3][4],可分解为三个一维数组,其数组名分别为:
-
这三个一维数组都有4个元素,例如:一维数组a[0]的 元素为a[0][0],a[0][1],a[0][2],a[0][3]。
-
所以遍历二维数组无非就是先取出二维数组中得一维数组, 然后再从一维数组中取出每个元素的值
-
示例
char cs[2][3] = {
{'a', 'b', 'c'},
{'d', 'e', 'f'}
};
printf("%c", cs[0][0]);// 第一个[0]取出一维数组, 第二个[0]取出一维数组中对应的元素
char cs[2][3] = {
{'a', 'b', 'c'},
{'d', 'e', 'f'}
};
for (int i = 0; i < 2; i++) { // 外循环取出一维数组
// i
for (int j = 0; j < 3; j++) {// 内循环取出一维数组的每个元素
printf("%c", cs[i][j]);
}
printf("\n");
}
注意: 必须强调的是,a[0],a[1],a[2]不能当作下标变量使用,它们是数组名,不是一个单纯的下标变量
二维数组的存储
-
和以为数组一样
-
给数组分配存储空间从内存地址大开始分配
-
给数组元素分配空间, 从所占用内存地址小的开始分配
-
往每个元素中存储数据从高地址开始存储
#include <stdio.h>
int main()
{
char cs[2][3] = {
{'a', 'b', 'c'},
{'d', 'e', 'f'}
};
// cs <span style="font-weight: bold;" class="mark"> &cs </span> &cs[0] == &cs[0][0]
printf("cs = %p\n", cs); // 0060FEAA
printf("&cs = %p\n", &cs); // 0060FEAA
printf("&cs[0] = %p\n", &cs[0]); // 0060FEAA
printf("&cs[0][0] = %p\n", &cs[0][0]); // 0060FEAA
return 0;
}
字符串
字符串(character string)是一个或多个字符的序列,双引号(“”)不是字符串的一部分。双引号仅告知编译器它括起来的是字符串,正如单引号用于标识单个字符一样。c语言没有专门用于存储字符串的变量类型,字符串都被存储在char类型的数组中。数组由连续的存储单元组成,字符串中的字符被存储在相邻的存储单元中,每个单元存储一个字符。
这是空字符(null character),C语言用它标记字符串的结束。空字符不是数字0,它是非打印字符,其ASCII码值是(或等价于0)。C中的字符串一定以空字符(\0)结束,这意味着数组的容量必须至少比待存储字符串中的字符数多1。
- 显式地为变量分配内存意味着你需要明确指定变量的类型和初始值。
对于全局变量和静态变量(在函数内部定义的静态变量和在函数外部定义的*全局变量*),它们的默认初始值为零。
以下是不同类型的变量在没有显示初始化时的默认值:
-
整型变量(int、short、long等):默认值为0。
-
浮点型变量(float、double等):默认值为0.0。
-
字符型变量(char):默认值为’\0’,即空字符。
-
指针变量:默认值为NULL,表示指针不指向任何有效的内存地址。
在C语言中,指针的大小取决于系统的体系结构和编译器的实现。
在32位系统中,指针的大小通常为4字节(32位 48),而在64位系统中,指针的大小通常为8字节(64位 8 8)。
-
数组、结构体、联合等复合类型的变量:它们的元素或成员将按照相应的规则进行默认初始化,这可能包括对元素递归应用默认规则。
需要注意的是,局部变量(在函数内部定义的非静态变量)不会自动初始化为默认值,它们的初始值是未定义的(包含垃圾值)。因此,在使用局部变量之前,应该为其赋予一个初始值。
总结起来,C 语言中变量的默认值取决于其类型和作用域。全局变量和静态变量(static)的默认值为 0,字符型变量的默认值为 \0,指针变量的默认值为 NULL,而局部变量没有默认值,其初始值是未定义的。
字符串常用方法
-
C语言中供了丰富的字符串处理函数,大致可分为字符串的输入、输出、合并、修改、比较、转 换、复制、搜索几类。
-
使用这些函数可大大减轻编程的负担。
-
使用输入输出的字符串函数,在使用前应包含头文件"stdio.h"
-
使用其它字符串函数则应包含头文件"string.h"
-
字符串输出函数:puts
-
格式: puts(字符数组名)
-
功能:把字符数组中的字符串输出到显示器。即在屏幕上显示该字符串。
-
优点:
-
自动换行
-
可以是数组的任意元素地址
-
缺点
-
不能自定义输出格式, 例如 puts(“hello %i”);
char ch[] = "lnj";
puts(ch); //输出结果: lnj
- puts函数完全可以由printf函数取代。当需要按一定格式输出时,通常使用printf函数
- 字符串输入函数:gets
- 格式: gets (字符数组名)
- 功能:从标准输入设备键盘上输入一个字符串。
char ch[30];
gets(ch); // 输入:lnj
puts(ch); // 输出:lnj
- 可以看出当输入的字符串中含有空格时,输出仍为全部字符串。说明gets函数并不以空格作为字符串输入结束的标志,而只以回车作为输入结束。这是与scanf函数不同的。
- 注意gets很容易导致数组下标越界,是一个不安全的字符串操作函数
-
字符串长度
-
利用sizeof字符串长度
-
因为字符串在内存中是逐个字符存储的,一个字符占用一个字节,所以字符串的结束符长度也是占用的内存单元的字节数。
char name[] = "it666";
int size = sizeof(name);// 包含\0
printf("size = %d\n", size); //输出结果:6
-
利用系统函数
-
格式: strlen(字符数组名)
-
功能:测字符串的实际长度(不含字符串结束标志‘\0’)并作为函数返回值。
char name[] = "it666";
size_t len = strlen(name2);
printf("len = %lu\n", len); //输出结果:5
- 以“\0”为字符串结束条件进行统计
/**
* 自定义方法计算字符串的长度
* @param name 需要计算的字符串
* @return 不包含\0的长度
*/
int myStrlen2(char str[])
{
// 1.定义变量保存字符串的长度
int length = 0;
while (str[length] != '\0')
{
length++;//1 2 3 4
}
return length;
}
/**
* 自定义方法计算字符串的长度
* @param name 需要计算的字符串
* @param count 字符串的总长度
* @return 不包含\0的长度
*/
int myStrlen(char str[], int count)
{
// 1.定义变量保存字符串的长度
int length = 0;
// 2.通过遍历取出字符串中的所有字符逐个比较
for (int i = 0; i < count; i++) {
// 3.判断是否是字符串结尾
if (str[i] == '\0') {
return length;
}
length++;
}
return length;
}
- 字符串连接函数:strcat
- 格式: strcat(字符数组名1,字符数组名2)
- 功能:把字符数组2中的字符串连接到字符数组1 中字符串的后面,并删去字符串1后的串标志 “\0”。本函数返回值是字符数组1的首地址。
char oldStr[100] = "welcome to";
char newStr[20] = " lnj";
strcat(oldStr, newStr);
puts(oldStr); //输出: welcome to lnj"
- 本程序把初始化赋值的字符数组与动态赋值的字符串连接起来。要注意的是,字符数组1应定义足 够的长度,否则不能全部装入被连接的字符串。
- 字符串拷贝函数:strcpy
- 格式: strcpy(字符数组名1,字符数组名2)
- 功能:把字符数组2中的字符串拷贝到字符数组1中。串结束标志“\0”也一同拷贝。字符数名2, 也可以是一个字符串常量。这时相当于把一个字符串赋予一个字符数组。
char oldStr[100] = "welcome to";
char newStr[50] = " lnj";
strcpy(oldStr, newStr);
puts(oldStr); // 输出结果: lnj // 原有数据会被覆盖
- 本函数要求字符数组1应有足够的长度,否则不能全部装入所拷贝的字符串。
-
字符串比较函数:strcmp
-
格式: strcmp(字符数组名1,字符数组名2)
-
功能:按照ASCII码顺序比较两个数组中的字符串,并由函数返回值返回比较结果。
-
字符串1=字符串2,返回值=0;
-
字符串1>字符串2,返回值>0;
-
字符串1<字符串2,返回值<0。
char oldStr[100] = "0";
char newStr[50] = "1";
printf("%d", strcmp(oldStr, newStr)); //输出结果:-1
char oldStr[100] = "1";
char newStr[50] = "1";
printf("%d", strcmp(oldStr, newStr)); //输出结果:0
char oldStr[100] = "1";
char newStr[50] = "0";
printf("%d", strcmp(oldStr, newStr)); //输出结果:1
字符串的基本概念
-
字符串是位于双引号中的字符序列
-
在内存中以“\0”结束,所占字节比实际多一个
字符串的初始化
- 在C语言中没有专门的字符串变量,通常用一个字符数组来存放一个字符串。
- 当把一个字符串存入一个数组时,会把结束符‘\0’存入数组,并以此作为该字符串是否结束的标志。
- 有了‘\0’标志后,就不必再用字符数组 的长度来判断字符串的长度了
- 初始化
char name[9] = "lnj"; //在内存中以“\0”结束, \0ASCII码值是0
char name1[9] = {'l','n','j','\0'};
char name2[9] = {'l','n','j',0};
// 当数组元素个数大于存储字符内容时, 未被初始化的部分默认值是0, 所以下面也可以看做是一个字符串
char name3[9] = {'l','n','j'};
- 错误的初始化方式
//省略元素个数时, 不能省略末尾的\n
// 不正确地写法,结尾没有\0 ,只是普通的字符数组
char name4[] = {'l','n','j'};
// "中间不能包含\0", 因为\0是字符串的结束标志
// \0的作用:字符串结束的标志
char name[] = "c\0ool";
printf("name = %s\n",name);
输出结果: c
字符串输出
-
如果字符数组中存储的是一个字符串, 那么字符数组的输入输出将变得简单方便。
-
不必使用循环语句逐个地输入输出每个字符
-
可以使用printf函数和scanf函数一次性输出输入一个字符数组中的字符串
-
使用的格式字符串为“%s”,表示输入、输出的是一个字符串 字符串的输出
-
输出
-
%s的本质就是根据传入的name的地址逐个去取数组中的元素然后输出,直到遇到\0位置
char chs[] = "lnj";
printf("%s\n", chs);
-
注意点:
-
\0引发的脏读问题
char name[] = {'c', 'o', 'o', 'l' , '\0'};
char name2[] = {'l', 'n', 'j'};
printf("name2 = %s\n", name2); // 输出结果: lnjcool
- 输入
char ch[10];
scanf("%s",ch);
-
注意点:
-
对一个字符串数组, 如果不做初始化赋值, 必须指定数组长度
-
ch最多存放由9个字符构成的字符串,其中最后一个字符的位置要留给字符串的结尾标示‘\0’
-
当用scanf函数输入字符串时,字符串中不能含有空格,否则将以空格作为串的结束符
字符
单个字符‘a’ 在内存中占用 1 字节。字符串“a“在内存中占用2字节,并且"a"的最后一个字符为’\0’,其中”a"是由a,/0构成,其中/0算一个字节,所以2个字节。中文,占2个字节。
(shorthand assignment opertor) 复合赋值运算符.
类型转换
类型转换是将一个数据类型的值转换为另一种数据类型的值。
(automatic type conversion )自动类型转换
C 语言中有两种类型转换:
- 隐式类型转换: 隐式类型转换是在表达式中自动发生的,无需进行任何明确的指令或函数调用。它通常是将一种较小的类型自动转换为较大的类型,例如,将int类型转换为long类型或float类型转换为double类型。隐式类型转换也可能会导致数据精度丢失或数据截断。
- 显式类型转换: 显式类型转换需要使用强制类型转换运算符(type casting operator),它可以将一个数据类型的值强制转换为另一种数据类型的值。强制类型转换可以使程序员在必要时对数据类型进行更精确的控制,但也可能会导致数据丢失或截断。
数值型数据之间的混合运算,就是不同类型数据在一起运算,其中必然会出现类型转换。
类型转换分为 自动类型转换与强制类型转换
注:
字节小的可以向字节大的自动转换,但字节大的不能向字节小的自动转换
char可以转换为int,int可以转换为double,char可以转换为double。但是不可以反向。
1.当类型转换出现在表达式时,无论是unsigned还是signed的char和short都会被自动转换成int,如有必要会被转换成unsigned int(如果short与int的大小相同,unsigned short就比int大。这种情况下,unsigned short会被转换成unsigned int)。由于都是从较小类型转换为较大类型,所以这些转换被称为升级(promotion)。
2.涉及两种类型的运算,两个值会被分别转换成两种类型的更高级别。
3.类型的级别从高至低依次是long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。例外的情况是,当long和int的大小相同时,unsigned int比long的级别高。之所以short和char类型没有列出,是因为它们已经被升级到int或unsigned int。
4.在赋值表达式语句中,计算的最终结果会被转换成被赋值变量的类型。这个过程可能导致类型升级或降级(demotion)。所谓降级,是指把一种类型转换成更低级别的类型。
5.当作为函数参数传递时,char和short被转换成int,float被转换成double。函数原型会覆盖自动升级。类型升级通常都不会有什么问题,但是类型降级会导致真正的麻烦。原因很简单:较低类型可能放不下整个数字。例如,一个8位的char类型变量存储整数101没问题,但是存不下22334。
强制类型转换
制类型转换
强制类型转换是通过定义类型转换运算来实现的。其一般形式为:
(数据类型) (表达式)
其作用是把表达式的运算结果强制转换成类型说明符所表示的类型
在使用强制转换时应注意以下问题:
数据类型和表达式都必须加括号, 如把(int)(x/2+y)写成(int)x/2+y则成了把x转换成int型之后再除2再与y相加了。
转换后不会改变原数据的类型及变量值,只在本次运算中临时性转换。
强制转换后的运算结果不遵循四舍五入原则。
例题:
-
- 有符号数与无符号数比较,会自动转换为无符号数,-1补码最高那位是1,比2大太多了
进制基本概念
-
什么是进制?
-
进制是一种计数的方式,数值的表示形式
-
常见的进制
-
十进制、二进制、八进制、十六进制
-
进制书写的格式和规律
-
十进制 0、1、2、3、4、5、6、7、8、9 逢十进一
-
二进制 0、1 逢二进一
-
书写形式:需要以0b或者0B开头,例如: 0b101
-
八进制 0、1、2、3、4、5、6、7 逢八进一
-
书写形式:在前面加个0,例如: 061
-
十六进制 0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F 逢十六进一
-
书写形式:在前面加个0x或者0X,例如: 0x45
-
练习
-
1.用不同进制表示如下有多少个方格
-
2.判断下列数字是否合理
00011 0x001 0x7h4 10.98 0986 .089-109 +178 0b325 0b0010 0xffdc 96f 96.0f 96.oF -.003
进制转换
-
10 进制转 2 进制
-
除2取余, 余数倒序; 得到的序列就是二进制表示形式
-
例如: 将十进制(97) 10转换为二进制数
-
2 进制转 10 进制
- 每一位二进制进制位的值 * 2的当前索引次幂; 再将所有位求出的值相加
- 例如: 将二进制01100100转换为十进制
01100100 索引从右至左, 从零开始 第0位: 0 * 2^0 = 0; 第1位: 0 * 2^1 = 0; 第2位: 1 * 2^2 = 4; 第3位: 0 * 2^3 = 0; 第4位: 0 * 2^4 = 0; 第5位: 1 * 2^5 = 32; 第6位: 1 * 2^6 = 64; 第7位: 0 * 2^7 = 0; 最终结果为: 0 + 0 + 4 + 0 + 0 + 32 + 64 + 0 = 100
-
2 进制转 8 进制
- 三个二进制位代表一个八进制位, 因为3个二进制位的最大值是7,而八进制是逢8进1
- 例如: 将二进制01100100转换为八进制数
从右至左每3位划分为8进制的1位, 不够前面补0 001 100 100 第0位: 100 等于十进制 4 第1位: 100 等于十进制 4 第2位: 001 等于十进制 1 最终结果: 144就是转换为8进制的值
-
2 进制转 16 进制
- 四个二进制位代表一个十六进制位,因为4个二进制位的最大值是15,而十六进制是逢16进1
- 例如: 将二进制01100100转换为十六进制数
从右至左每4位划分为16进制的1位, 不够前面补0 0110 0100 第0位: 0100 等于十进制 4 第1位: 0110 等于十进制 6 最终结果: 64就是转换为16进制的值
-
其它进制转换为十进制
- 系数 * 基数 ^ 索引 之和
十进制 --> 十进制 12345 = 10000 + 2000 + 300 + 40 + 5 = (1 * 10 ^ 4) + (2 * 10 ^ 3) + (3 * 10 ^ 2) + (4 * 10 ^ 1) + (5 * 10 ^ 0) = (1 * 10000) + (2 + 1000) + (3 * 100) + (4 * 10) + (5 * 1) = 10000 + 2000 + 300 + 40 + 5 = 12345 规律: 其它进制转换为十进制的结果 = 系数 * 基数 ^ 索引 之和 系数: 每一位的值就是一个系数 基数: 从x进制转换到十进制, 那么x就是基数 索引: 从最低位以0开始, 递增的数
二进制 --> 十进制 543210 101101 = (1 * 2 ^ 5) + (0 * 2 ^ 4) + (1 * 2 ^ 3) + (1 * 2 ^ 2) + (0 * 2 ^ 1) + (1 * 2 ^ 0) = 32 + 0 + 8 + 4 + 0 + 1 = 45 八进制 --> 十进制 016 = (0 * 8 ^ 2) + (1 * 8 ^ 1) + (6 * 8 ^ 0) = 0 + 8 + 6 = 14 十六进制 --> 十进制 0x11f = (1 * 16 ^ 2) + (1 * 16 ^ 1) + (15 * 16 ^ 0) = 256 + 16 + 15 = 287
-
十进制快速转换为其它进制
- 十进制除以
基数
取余, 倒叙读取
十进制 --> 二进制 100 --> 1100100 100 / 2 = 50 0 50 / 2 = 25 0 25 / 2 = 12 1 12 / 2 = 6 0 6 / 2 = 3 0 3 / 2 = 1 1 1 / 2 = 0 1 十进制 --> 八进制 100 --> 144 100 / 8 = 12 4 12 / 8 = 1 4 1 / 8 = 0 1 十进制 --> 十六进制 100 --> 64 100 / 16 = 6 4 6 / 16 = 0 6
- 十进制除以
十进制小数转换为二进制小数
-
整数部分,直接转换为二进制即可
-
小数部分,使用"乘2取整,顺序排列"
- 用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,直到积中的小数部分为零,或者达到所要求的精度为止
- 然后把取出的整数部分按顺序排列起来, 即是小数部分二进制
-
最后将整数部分的二进制和小数部分的二进制合并起来, 即是一个二进制小数
-
例如: 将12.125转换为二进制
// 整数部分(除2取余)
12
/ 2
------
6 // 余0
/ 2
------
3 // 余0
/ 2
------
1 // 余1
/ 2
------
0 // 余1
//12 --> 1100
// 小数部分(乘2取整数积)
0.125
* 2
------
0.25 //0
0.25
* 2
------
0.5 //0
0.5
* 2
------
1.0 //1
0.0
// 0.125 --> 0.001
// 12.8125 --> 1100.001
二进制小数转换为十进制小数
- 整数部分按照二进制转十进制即可
- 小数部分从最高位开始乘以2的负n次方, n从1开始
- 例如: 将 1100.001转换为十进制
// 整数部分(乘以2的n次方, n从0开始)
0 * 2^0 = 0
0 * 2^1 = 0
1 * 2^2 = 4
1 * 2^3 = 8
// 1100 <span style="font-weight: bold;" class="mark"> 8 + 4 + 0 + 0 </span> 12
// 小数部分(乘以2的负n次方, n从0开始)
0 * (1/2) = 0
0 * (1/4) = 0
1 * (1/8) = 0.125
// .100 <span style="font-weight: bold;" class="mark"> 0 + 0 + 0.125 </span> 0.125
// 1100.001 --> 12.125
-
练习:
- 将0.8125转换为二进制
- 将0.1101转换为十进制
0.8125
* 2
--------
1.625 // 1
0.625
* 2
--------
1.25 // 1
0.25
* 2
--------
0.5 // 0
* 2
--------
1.0 // 1
0.0
// 0. 8125 --> 0.1101
1*(1/2) = 0.5
1*(1/4)=0.25
0*(1/8)=0
1*(1/16)=0.0625
//0.1101 --> 0.5 + 0.25 + 0 + 0.0625 == 0.8125
原码反码补码
-
计算机只能识别0和1, 所以计算机中存储的数据都是以0和1的形式存储的
-
数据在计算机内部是以补码的形式储存的, 所有数据的运算都是以补码进行的
-
正数的原码、反码和补码
-
正数的原码、反码和补码都是它的二进制
-
例如: 12的原码、反码和补码分别为
-
0000 0000 0000 0000 0000 0000 0000 1100
-
0000 0000 0000 0000 0000 0000 0000 1100
-
0000 0000 0000 0000 0000 0000 0000 1100
-
-
-
负数的原码、反码和补码
- 二进制的最高位我们称之为符号位, 最高位是0代表是一个正数, 最高位是1代表是一个负数
- 一个负数的原码, 是将该负数的二进制最高位变为1
- 一个负数的反码, 是将该数的原码
除了符号位
以外的其它位取反 - 一个负数的补码, 就是它的反码 + 1
- 例如: -12的原码、反码和补码分别为
0000 0000 0000 0000 0000 0000 0000 1100 // 12二进制 1000 0000 0000 0000 0000 0000 0000 1100 // -12原码 1111 1111 1111 1111 1111 1111 1111 0011 // -12反码 1111 1111 1111 1111 1111 1111 1111 0100 // -12补码
-
负数的原码、反码和补码逆向转换
- 反码 = 补码-1
- 原码= 反码最高位不变, 其它位取反
1111 1111 1111 1111 1111 1111 1111 0100 // -12补码 1111 1111 1111 1111 1111 1111 1111 0011 // -12反码 1000 0000 0000 0000 0000 0000 0000 1100 // -12原码
-
为什么要引入反码和补码
-
在学习本节内容之前,大家必须明白一个东西, 就是计算机只能做加法运算, 不能做减法和乘除法, 所以的减法和乘除法内部都是用加法来实现的
- 例如: 1 - 1, 内部其实就是 1 + (-1);
- 例如: 3 * 3, 内部其实就是 3 + 3 + 3;
- 例如: 9 / 3, 内部其实就是 9 + (-3) + (-3) + (-3);
-
首先我们先来观察一下,如果只有原码会存储什么问题
- 很明显, 通过我们的观察, 如果只有原码, 1-1的结果不对
// 1 + 1 0000 0000 0000 0000 0000 0000 0000 0001 // 1原码 +0000 0000 0000 0000 0000 0000 0000 0001 // 1原码 --------------------------------------- 0000 0000 0000 0000 0000 0000 0000 0010 <span style="font-weight: bold;" class="mark"> 2 // 1 - 1; 1 + (-1); 0000 0000 0000 0000 0000 0000 0000 0001 // 1原码 +1000 0000 0000 0000 0000 0000 0000 0001 // -1原码 --------------------------------------- 1000 0000 0000 0000 0000 0000 0000 0010 </span> -2
-
-
正是因为对于减法来说,如果使用原码结果是不正确的, 所以才引入了反码
- 通过反码计算减法的结果, 得到的也是一个反码;
- 将计算的结果符号位不变其余位取反,就得到了计算结果的原码
- 通过对原码的转换, 很明显我们计算的结果是-0, 符合我们的预期
// 1 - 1; 1 + (-1);
0000 0000 0000 0000 0000 0000 0000 0001 // 1反码
1111 1111 1111 1111 1111 1111 1111 1110 // -1反码
---------------------------------------
1111 1111 1111 1111 1111 1111 1111 1111 // 计算结果反码
1000 0000 0000 0000 0000 0000 0000 0000 // 计算结果原码 <span style="font-weight: bold;" class="mark"> -0
-
虽然反码能够满足我们的需求, 但是对于0来说, 前面的负号没有任何意义, 所以才引入了补码
- 由于int只能存储4个字节, 也就是32位数据, 而计算的结果又33位, 所以最高位溢出了,符号位变成了0, 所以最终得到的结果是0
// 1 - 1; 1 + (-1);
0000 0000 0000 0000 0000 0000 0000 0001 // 1补码
1111 1111 1111 1111 1111 1111 1111 1111 // -1补码
---------------------------------------
10000 0000 0000 0000 0000 0000 0000 0000 // 计算结果补码
0000 0000 0000 0000 0000 0000 0000 0000 // </span> 0
类型说明符
-
类型说明符基本概念
-
C语言提供了说明长度和说明符号位的两种类型说明符, 这两种类型说明符一共有4个:
-
short 短整型 (说明长度)
-
long 长整型 (说明长度)
-
signed 有符号型 (说明符号位)
-
unsigned 无符号型 (说明符号位)
-
这些说明符一般都是用来修饰int类型的,所以在使用时可以省略int
-
这些说明符都属于C语言关键字
short和long
-
short和long可以提供不同长度的整型数,也就是可以改变整型数的取值范围。
-
在64bit编译器环境下,int占用4个字节(32bit),取值范围是-2^31 ~ 2^31-1;
-
short占用2个字节(16bit),取值范围是-2^15 ~ 2^15-1;
-
long占用8个字节(64bit),取值范围是-2^63 ~ 2^63-1
-
总结一下:在64位编译器环境下:
-
short占2个字节(16位)
-
int占4个字节(32位)
-
long占8个字节(64位)。
-
因此,如果使用的整数不是很大的话,可以使用short代替int,这样的话,更节省内存开销。
-
世界上的编译器林林总总,不同编译器环境下,int、short、long的取值范围和占用的长度又是不一样的。比如在16bit编译器环境下,long只占用4个字节。不过幸运的是,ANSI \ ISO制定了以下规则:
-
short跟int至少为16位(2字节)
-
long至少为32位(4字节)
-
short的长度不能大于int,int的长度不能大于long
-
char一定为为8位(1字节),毕竟char是我们编程能用的最小数据类型
-
可以连续使用2个long,也就是long long。一般来说,long long的范围是不小于long的,比如在32bit编译器环境下,long long占用8个字节,long占用4个字节。不过在64bit编译器环境下,long long跟long是一样的,都占用8个字节。
#include <stdio.h>
int main()
{
// char占1个字节, char的取值范围 -2^7~2^7
char num = 129;
printf("size = %i\n", sizeof(num)); // 1
printf("num = %i\n", num); // -127
// short int 占2个字节, short int的取值范围 -2^15~2^15-1
short int num1 = 32769;// -32767
printf("size = %i\n", sizeof(num1)); // 2
printf("num1 = %hi\n", num1);
// int占4个字节, int的取值范围 -2^31~2^31-1
int num2 = 12345678901;
printf("size = %i\n", sizeof(num2)); // 4
printf("num2 = %i\n", num2);
// long在32位占4个字节, 在64位占8个字节
long int num3 = 12345678901;
printf("size = %i\n", sizeof(num3)); // 4或8
printf("num3 = %ld\n", num3);
// long在32位占8个字节, 在64位占8个字节 -2^63~2^63-1
long long int num4 = 12345678901;
printf("size = %i\n", sizeof(num4)); // 8
printf("num4 = %lld\n", num4);
// 由于short/long/long long一般都是用于修饰int, 所以int可以省略
short num5 = 123;
printf("num5 = %lld\n", num5);
long num6 = 123;
printf("num6 = %lld\n", num6);
long long num7 = 123;
printf("num7 = %lld\n", num7);
return 0;
}
signed和unsigned
-
首先要明确的:signed int等价于signed,unsigned int等价于unsigned
-
signed和unsigned的区别就是它们的最高位是否要当做符号位,并不会像short和long那样改变数据的长度,即所占的字节数。
-
signed:表示有符号,也就是说最高位要当做符号位。但是int的最高位本来就是符号位,因此signed和int是一样的,signed等价于signed int,也等价于int。signed的取值范围是-2^31 ~ 2^31 - 1
-
unsigned:表示无符号,也就是说最高位并不当做符号位,所以不包括负数。
-
因此unsigned的取值范围是:0000 0000 0000 0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 1111 1111 1111,也就是0 ~ 2^32 - 1
#include <stdio.h>
int main()
{
// 1.默认情况下所有类型都是由符号的
int num1 = 9;
int num2 = -9;
int num3 = 0;
printf("num1 = %i\n", num1);
printf("num2 = %i\n", num2);
printf("num3 = %i\n", num3);
// 2.signed用于明确说明, 当前保存的数据可以是有符号的, 一般情况下很少使用
signed int num4 = 9;
signed int num5 = -9;
signed int num6 = 0;
printf("num4 = %i\n", num4);
printf("num5 = %i\n", num5);
printf("num6 = %i\n", num6);
// signed也可以省略数据类型, 但是不推荐这样编写
signed num7 = 9;
printf("num7 = %i\n", num7);
// 3.unsigned用于明确说明, 当前不能保存有符号的值, 只能保存0和正数
// 应用场景: 保存银行存款,学生分数等不能是负数的情况
unsigned int num8 = -9;
unsigned int num9 = 0;
unsigned int num10 = 9;
// 注意: 不看怎么存只看怎么取
printf("num8 = %u\n", num8);
printf("num9 = %u\n", num9);
printf("num10 = %u\n", num10);
return 0;
}
-
注意点:
-
修饰符号的说明符可以和修饰长度的说明符混合使用
-
相同类型的说明符不能混合使用
signed short int num1 = 666;
signed unsigned int num2 = 666; // 报错
char类型内存存储细节
-
char类型基本概念
-
char是C语言中比较灵活的一种数据类型,称为“字符型”
-
char类型变量占1个字节存储空间,共8位
-
除单个字符以外, C语言的的转义字符也可以利用char类型存储
字符 | 意义 |
---|---|
\b | 退格(BS)当前位置向后回退一个字符 |
\r | 回车(CR),将当前位置移至本行开头 |
\n | 换行(LF),将当前位置移至下一行开头 |
\t | 水平制表(HT),跳到下一个 TAB 位置 |
\0 | 用于表示字符串的结束标记 |
\ | 代表一个反斜线字符 \ |
\" | 代表一个双引号字符" |
\’ | 代表一个单引号字符’ |
-
char型数据存储原理
-
计算机只能识别0和1, 所以char类型存储数据并不是存储一个字符, 而是将字符转换为0和1之后再存储
-
正是因为存储字符类型时需要将字符转换为0和1, 所以为了统一, 老美就定义了一个叫做ASCII表的东东
-
ASCII表中定义了每一个字符对应的整数
char ch1 = 'a'; printf("%i\n", ch1); // 97 char ch2 = 97; printf("%c\n", ch2); // a
-
char类型注意点
-
char类型占一个字节, 一个中文字符占3字节(unicode表),所有char不可以存储中文
char c = '我'; // 错误写法
-
除转义字符以外, 不支持多个字符
char ch = 'ab'; // 错误写法
-
char类型存储字符时会先查找对应的ASCII码值, 存储的是ASCII值, 所以字符6和数字6存储的内容不同
char ch1 = '6'; // 存储的是ASCII码 64char ch2 = 6; // 存储的是数字 6
数组的基本概念
-
数组,从字面上看,就是一组数据的意思,没错,数组就是用来存储一组数据的
- 在C语言中,数组属于构造数据类型
-
数组的几个名词
- 数组:一组
相同数据类型
数据的有序
的集合 - 数组元素: 构成数组的每一个数据。
- 数组的下标: 数组元素位置的索引(从0开始)
- 数组:一组
-
数组的应用场景
-
一个int类型的变量能保存一个人的年龄,如果想保存整个班的年龄呢?
- 第一种方法是定义很多个int类型的变量来存储
- 第二种方法是只需要定义一个int类型的数组来存储
-
#include <stdio.h>
int main(int argc, const char * argv[]) {
/*
// 需求: 保存2个人的分数
int score1 = 99;
int score2 = 60;
// 需求: 保存全班同学的分数(130人)
int score3 = 78;
int score4 = 68;
...
int score130 = 88;
*/
// 数组: 如果需要保存`一组``相同类型`的数据, 就可以定义一个数组来保存
// 只要定义好一个数组, 数组内部会给每一块小的存储空间一个编号, 这个编号我们称之为 索引, 索引从0开始
// 1.定义一个可以保存3个int类型的数组
int scores[3];
// 2.通过数组的下标往数组中存放数据
scores[0] = 998;
scores[1] = 123;
scores[2] = 567;
// 3.通过数组的下标从数组中取出存放的数据
printf("%i\n", scores[0]);
printf("%i\n", scores[1]);
printf("%i\n", scores[2]);
return 0;
}
定义数组
- 元素类型 数组名[元素个数];
// int 元素类型
// ages 数组名称
// [10] 元素个数
int ages[10];
初始化数组
-
定义的同时初始化
-
指定元素个数,完全初始化
- 其中在{ }中的各数据值即为各元素的初值,各值之间用逗号间隔
int ages[3] = {4, 6, 9};
-
不指定元素个数,完全初始化
- 根据大括号中的元素的个数来确定数组的元素个数
int nums[] = {1,2,3,5,6};
-
指定元素个数,部分初始化
- 没有显式初始化的元素,那么系统会自动将其初始化为0
int nums[10] = {1,2};
- 指定元素个数,部分初始化
int nums[5] = {[4] = 3,[1] = 2};
- 不指定元素个数,部分初始化
int nums[] = {[4] = 3};
- 先定义后初始化
int nums[3];
nums[0] = 1;
nums[1] = 2;
nums[2] = 3;
-
没有初始化会怎样?
- 如果定义数组后,没有初始化,数组中是有值的,是随机的垃圾数,所以如果想要正确使用数组应该要进行初始化。
int nums[5];
printf("%d\n", nums[0]);
printf("%d\n", nums[1]);
printf("%d\n", nums[2]);
printf("%d\n", nums[3]);
printf("%d\n", nums[4]);
输出结果:
0
0
1606416312
0
1606416414
- 注意点:
- 使用数组时不能超出数组的索引范围使用, 索引从0开始, 到元素个数-1结束
- 使用数组时不要随意使用未初始化的元素, 有可能是一个随机值
- 对于数组来说, 只能在定义的同时初始化多个值, 不能先定义再初始化多个值
int ages[3];
ages = {4, 6, 9}; // 报错
数组的使用
- 通过下标(索引)访问:
// 找到下标为0的元素, 赋值为10
ages[0]=10;
// 取出下标为2的元素保存的值
int a = ages[2];
printf("a = %d", a);
数组的遍历
- 数组的遍历:遍历的意思就是有序地查看数组的每一个元素
int ages[4] = {19, 22, 33, 13};
for (int i = 0; i < 4; i++) {
printf("ages[%d] = %d\n", i, ages[i]);
}
数组长度计算方法
-
因为数组在内存中占用的字节数取决于其存储的数据类型和数据的个数
- 数组所占用存储空间 = 一个元素所占用存储空间 * 元素个数(数组长度)
-
所以计算数组长度可以使用如下方法
数组的长度 = 数组占用的总字节数 / 数组元素占用的字节数
int ages[4] = {19, 22, 33, 13};
int length = sizeof(ages)/sizeof(int);
printf("length = %d", length);
输出结果: 4
数组内部存储细节
-
存储方式:
-
1)内存寻址从大到小, 从高地址开辟一块连续没有被使用的内存给数组
-
2)从分配的连续存储空间中, 地址小的位置开始给每个元素分配空间
-
3)从每个元素分配的存储空间中, 地址最大的位置开始存储数据
-
4)用数组名指向整个存储空间最小的地址
-
示例
#include <stdio.h>
int main()
{
int num = 9;
char cs[] = {'l','n','j'};
printf("cs = %p\n", &cs); // cs = 0060FEA9
printf("cs[0] = %p\n", &cs[0]); // cs[0] = 0060FEA9
printf("cs[1] = %p\n", &cs[1]); // cs[1] = 0060FEAA
printf("cs[2] = %p\n", &cs[2]); // cs[2] = 0060FEAB
int nums[] = {2, 6};
printf("nums = %p\n", &nums); // nums = 0060FEA0
printf("nums[0] = %p\n", &nums[0]);// nums[0] = 0060FEA0
printf("nums[1] = %p\n", &nums[1]);// nums[1] = 0060FEA4
return 0;
}
- 注意:字符在内存中是以对应ASCII码值的二进制形式存储的,而非上述的形式。
数组的越界问题
-
数组越界导致的问题
-
约错对象
-
程序崩溃
char cs1[2] = {1, 2};
char cs2[3] = {3, 4, 5};
cs2[3] = 88; // 注意:这句访问到了不属于cs1的内存
printf("cs1[0] = %d\n", cs1[0] );
输出结果: 88
为什么上述会输出88, 自己按照"数组内部存储细节"画图脑补
数组注意事项
- 在定义数组的时候[]里面只能写整型常量或者是返回整型常量的表达式
int ages4['A'] = {19, 22, 33};
printf("ages4[0] = %d\n", ages4[0]);
int ages5[5 + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);
int ages5['A' + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);
- 错误写法
// 没有指定元素个数,错误
int a[];
// []中不能放变量
int number = 10;
int ages[number]; // 老版本的C语言规范不支持
printf("%d\n", ages[4]);
int number = 10;
int ages2[number] = {19, 22, 33} // 直接报错
// 只能在定义数组的时候进行一次性(全部赋值)的初始化
int ages3[5];
ages10 = {19, 22, 33};
// 一个长度为n的数组,最大下标为n-1, 下标范围:0~n-1
int ages4[4] = {19, 22, 33}
ages4[8]; // 数组角标越界
数组和函数
-
数组可以作为函数的参数使用,数组用作函数参数有两种形式:
- 一种是把数组元素作为实参使用
- 一种是把数组名作为函数的形参和实参使用
数组元素作为函数参数
- 数组的元素作为函数实参,与同类型的简单变量作为实参一样,如果是基本数据类型, 那么形参的改变不影响实参
void change(int val)// int val = number
{
val = 55;
}
int main(int argc, const char * argv[])
{
int ages[3] = {1, 5, 8};
printf("ages[0] = %d", ages[0]);// 1
change(ages[0]);
printf("ages[0] = %d", ages[0]);// 1
}
- 用数组元素作函数参数不要求形参也必须是数组元素
数组名作为函数参数
- 在C语言中,数组名除作为变量的标识符之外,数组名还代表了该数组在内存中的起始地址,因此,当数组名作函数参数时,实参与形参之间不是"值传递",而是"地址传递"
- 实参数组名将该数组的起始地址传递给形参数组,两个数组共享一段内存单元, 系统不再为形参数组分配存储单元
- 既然两个数组共享一段内存单元, 所以形参数组修改时,实参数组也同时被修改了
void change2(int array[3])// int array = 0ffd1
{
array[0] = 88;
}
int main(int argc, const char * argv[])
{
int ages[3] = {1, 5, 8};
printf("ages[0] = %d", ages[0]);// 1
change(ages);
printf("ages[0] = %d", ages[0]);// 88
}
数组名作函数参数的注意点
- 在函数形参表中,允许不给出形参数组的长度
void change(int array[])
{
array[0] = 88;
}
- 形参数组和实参数组的类型必须一致,否则将引起错误。
void prtArray(double array[3]) // 错误写法
{
for (int i = 0; i < 3; i++) {
printf("array[%d], %f", i, array[i]);
}
}
int main(int argc, const char * argv[])
{
int ages[3] = {1, 5, 8};
prtArray(ages[0]);
}
- 当数组名作为函数参数时, 因为自动转换为了指针类型,所以在函数中无法动态计算除数组的元素个数
void printArray(int array[])
{
printf("printArray size = %lu\n", sizeof(array)); // 8
int length = sizeof(array)/ sizeof(int); // 2
printf("length = %d", length);
}
contrl expession(控制表达式)