数据类型与变量

打印语句

1
2
3
4
5
6
7
#include<stdio.h>

int main() {
printf("%d\n", 2); // 2
printf("%f\n", 2); // 0.000000
printf("%d\n", '2;); // 50
}

这里使用了%f来输出一个浮点数,但传入的是一个整数 2。在大多数实现中,整数会被隐式转换为浮点数(即 2.000000);但是,由于 printf 的行为可能依赖于具体的编译器和运行时环境,有时可能看到不同的输出(比如由于浮点数表示的精度问题)。

字符 ‘2’ 的 ASCII 值是 50

字面量的类型

程序中的数值(字面量)也有类型:默认类型或者指定类型

  • 默认类型:2 为 int,0.2 为 double, ‘c’ 为 char
  • 指定类型:0.2f 为 float(后缀 f 表示 float)

C 语言是类型严格的语言,字面量也有类型,使用字面量时需要考虑类型;

1
2
3
4
5
short s = 2; // 字面量 2 的类型被转换为 short
double d = 2; // 字面量 2 的类型会被转为 double

long n = -1l; // 表示该字面量为 lang 类型
long long nl = 2147483648u; // 表示该字面量为无符号类型

变量之间的赋值

  • 大类型赋值给小类型时:可能会发生溢出

    • 当数值在小类型范围内 -> 赋值成功
    • 当数值超过小类型的范围 -> 发生溢出
  • 小类型可以安全的赋值给大类型

  • 浮点类型赋值给整形,会发生截断(小数部分丢失)

  • 整数赋值给浮点类型,能够完成(会在后面补 0)

signed 与 unsigned

有符号与无符号

  • signed int
    • 范围:-2147483648 ~ 2147483647
  • unsigned int
    • 范围:0 ~ 4294967295

扩展

  • signed 和 unsigned 可与 char 和 short 组合使用
    • signed char,unsigned char
    • signed short,unsigned short
  • 程序中可能出现的关于 int 的缩写
    • signed <-> signed int
    • unsigned <-> unsigned int

打印无符号整形时候要使用 %u 而不是%d,%d 会依旧按照有符号进行操作;

类型本身是不会占用内存的,只有变量才会占用内存

sizeof关键字

功能:用于获取类型或者变量所占用的内存大小(字节)

1
2
3
4
5
int main() {
int s = sizeof(short); // 使用 shrot 类型所表示的字节大小来初始化 s
int t = sizeof s; // 使用变量 s 所作占用的字节大小来初始化 t
int u = sizeof(s); // 同上:使用变量 s 所作占用的字节大小来初始化 t
}

long

  • long 在使用不同编译器时,可能占用的内存不同
  • long 通常占用 4 个字节,也可能占用 8 个字节
  • long long 表示整形,固定占用 8 个字节
  • long long 是 long long int 的缩写形式

浮点数

  • 浮点类型(float & double)对数据的表示是不准确的
  • 整数类型(char,short,int,long)对数据的表示是准确的
  • 浮点类型与整数类型在内存中对数据的表示法完全不同

变量的作用域

C 语言中变量的分类

  • 局部变量
    • 函数内部定义的变量(隶属于当前函数)
    • 只能在当前函数中进行使用
  • 全局变量
    • 全局范围内的函数(不特定隶属于任意一个函数)
    • 可以在任意函数中访问使用

同名变量的问题

  • 不同函数中的局部变量可以同名(不会产生冲突)
  • 全局变量不能同名(会产生冲突)
  • 当局部变量和全局变量同名时,优先使用局部变量

全局变量的作用域

  • 全局作用域:可以在程序的各个角落访问并使用(不限于当前文件中)

  • 文件作用域:只能在当前代码文件中访问并使用

  • 全局变量的作用域可能被局部变量覆盖(同名局部变量)

  • 工程开发中,全局变量通常以 g_ 作为前缀命名(工程约定)

不同变量的物理存储区域

  • 在现代计算机系统中,物理内存被分为不同区域
  • 区域不同,用途不同,不同种类的变量位于不同区域
    • 全局数据区域:存放全局变量,静态变量,其中的变量默认初始化为0
    • 栈空间:存储函数参数,局部变量
    • 堆空间:用于动态创建变量
  • 不同变量的生命期
    • 全局数据区中的变量
      • 程序开始运行时创建,程序结束时被销毁,整个程序运行期合法可用
    • 栈空间中的变量
      • 进入作用域时创建,离开作用域时销毁(自动销毁)
        • 函数中的局部变量在函数调用返回后销毁
    • 作用域和生命周期二者并无本质联系,只是语法层面(作用域)和二进制层面(生命周期)的两个说法

静态变量

  • static 是 C 语言中的关键字
  • static 修饰的局部变量创建于全局数据区(拥有程序生命周期)
  • static 修饰的全局变量只有文件作用域(超出对应文件外无法访问,作用域被缩小了)
  • static 局部变量只会初始化一次,作用域与普通变量无异

对于局部变量而言,static 只是改变了它的生命周期,其本身的作用域不变

变量的声明周期

变量的生命周期由变量的存储位置决定

  • static 将变量存储于全局数据区,默认初始化为 0
  • auto 将变量存储于栈空间,默认初始化为随机值
  • register 将变量存储于寄存器,默认初始化随机值(因为寄存器太少了,所以存储不一定成功,故实际中基本不用)

自定义数据类型

typedef

类型命名关键字(typedef)

  • C 语言中可以对类型赋予新名字
  • 语法 typedef Type NewTypeName;
    • 注意:typedef 并没有创建新类型,只是创建了类型别名

应用

  • typedef 可在函数中定义”局部类型名“
  • typedef 常用于简化类型名(如:unsigned long long)
  • typedef 定义类型名,能够以统一方式创建变量(Type var;)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<stdio.h>

// 为 float [5] 起个新名字为: FArr5
typedef float(FArr5)[5];
// 为 int()(int, int) 起个新名字为: FArr5
typedef int(IFuncII)(int, int);

/*
typedef float(*FArr5)[5];
typedef int(*IFuncII)(int, int);
如何这么写的话,则:
FArr5 pa = &g_arr;
IFuncII pf = add;
须这样写
*/

float g_arr[5] = {0.1, 0.2, 0.3};

int add(int a, int b) {
return a + b;
}

int main() {

FArr5* pa = &g_arr;
IFuncII* pf = add;

int i = 0;

for(i = 0; i < 5; i++){
printf("%f\n", (*pa)[i]);
}

printf("%d\n", pf(i, i + 1));

return 0 ;
}
/*
0.100000
0.200000
0.300000
0.000000
0.000000
11
*/

struct

是能够定义不同数据类型变量的集合类型

  • struct 结构体变量的本质是变量的集合
  • struct 结构体变量中的成员占用独立的内存
  • struct 结构体可以用 typedef 赋予新类型名
  • 可定义 struct 结构体类型的指针,并指向对应类型的变量

jiegoutiyongfa

深入 struct 结构体类型

  • struct 结构体类型可以先前置声明,在具体定义;
    • struct test;
  • 前置类型声明只能用于定义指针;
    • struct test* p
  • 类型完整定义之后才能进行变量定义
  • struct 结构体类型可以省略类型名
    • 省略类型名时,每次创建变脸必须给出完整结构体定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>

struct Test;
struct Test* g_pt;

int main() {
// 必须先给出类型的完整定义才能创建相应类型的变量
struct Test t;
t.a = 1;
t.b = 2;

g_pt = &t;

return 0;
}

struct Test
{
int a;
int b;
};
  • struct 结构体类型可以省略类型名(无名结构体类型)
  • 类型名省略时,每次创建变量必须给出完整结构体定义
  • 无名结构体类型总是互不相同的类型(互不兼容)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>

int main() {
struct {int a, b;} v1;
struct {int a, b;} v2;
struct {int a, b;}* pv;

v1.a = 1;
v1.b = 2;

// v2 = v1 // Error 类型不同互不兼容

//pv = &v1; // WARNING 类型不同互不兼容

return 0 ;

}

位域

  • 现代程序设计中,内存使用的最小单位为字节(约定俗成)
  • 在一些特定场合中,可将比特作为最小单位使用内存
  • 结构体类型能够指定成员变量占用内存的比特位宽度(位域)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<stdio.h>

struct BW {
unsigned char a : 4; // a 占用一个字节的 4 位款组 二进制的 1111 即:[0, 15]
unsigned char b : 2; // b 占用一个字节的 2 位款组
unsigned char c : 2; // c 占用一个字节的 2 位款组
};

int main() {

struct BW bw;
bw.a = 10;
bw.b = 1; // 因为大小为2bit,所以如果赋值超过2bit则会发生溢出回转
bw.c = 3;

printf("sizeof(struct BW) = %d\n", sizeof(struct BW));
printf("bw.a = %d\n", bw.a);
printf("bw.b = %d\n", bw.b);
printf("bw.c = %d\n", bw.c);

return 0 ;
}
/*
sizeof(struct BW) = 1
bw.a = 10
bw.b = 1
bw.c = 3
*/

深入位域

  • 位域成员必须是整形,默认情况下成员依次排列
  • 位域成员占用的位数不能超过类型宽度(错误示例:char c : 9)
  • 当存储位不足时,自动启用新存储单元
    • char a : 7; char b : 6; 两个加起来超过了一个字节的大小,那么它将会自动启用一个新的字节来存储当前内容
  • 可以舍弃当前未使用的位,重新启用存储单元
1
2
3
4
5
struct Bits1 {
unsigned cahr a : 4;
unsigned cahr : 0;
unsigned cahr b : 4;
}

unsigned char : 0; 这行代码,其作用主要是作为填充或对齐。当你在结构体中声明一个大小为0的位域时,它实际上不占用任何位(即,它不存在于内存布局中),但它可以影响结构体中后续位域的对齐方式。

a占了一个字节的前4位,b正好可以占剩下的后四位,刚刚好一个字节;但unsigned char : 0; 这行代码会将 a 所在字节的后四位进行占位,b 只能重新分配一个新的字节并占前四位;

  • unsigned char : 0;:这定义了一个大小为0的位域。在大多数上下文中,这样的位域实际上不占用任何位(即,它不会为结构体增加任何额外的内存负担),并且它的存在主要是为了可能的对齐效果或作为占位符。然而,需要注意的是,编译器如何处理这种0大小的位域可能依赖于编译器的具体实现,并且可能不会产生预期的效果。
  • unsigned char : 4;:这定义了一个大小为4位的位域。这意味着这个位域将占用结构体中的4个位,用于存储数据。在这个例子中,由于使用了 unsigned char 类型(尽管在定义位域时类型主要影响解释方式,而不是实际占用的内存大小),这4个位可以用来存储从0到15(即 0b00000b1111)的整数值。

联合体 Union

语法

1
2
3
4
5
6
union TypeName {
Type1 var1;
Type2 var2;
...
TypeN varn;
};

union 与 struct 的不同

  • union 类型所有成员共享一段内存(所有成员起始地址相同)
  • union 类型的大小取决于成员的最大类型
  • union 类型的变量只能以第一个成员类型的有效值进行初始化
1
2
3
4
5
6
union UT {
int a;
char b;
};

union UT uu = {0};

联合体中的所有成员共享一段内存,即:如果 uu.a = 1,那么 uu.b 也等于 1;

union 类型的大小取决于成员的最大类型,即:uu 这个联合体的大小为 a 这个字段的大小,也就是 4 字节;

union 类型的变量只能以第一个成员类型的有效值进行初始化,即:union UT uu = {0}; 0 是 a 的 int 类型;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<stdio.h>

union UTest {
int a;
float f;
};

void main() {

union UTest t = {987654321};

printf("sizeof(union UTest) = %d\n", sizeof(union UTest));
printf("&t.a = %p\n", &t.a);
printf("&t.f = %p\n", &t.f);

printf("t.a = %d\n", t.a);
printf("t.f = %f\n", t.f);

t.f = 987654321.0f;

printf("t.a = %d\n", t.a);
printf("t.f = %f\n", t.f);
}
/*
sizeof(union UTest) = 4
&t.a = 0x7ffc28637ea4
&t.f = 0x7ffc28637ea4
t.a = 987654321
t.f = 0.001697
t.a = 1315666339
t.f = 987654336.000000
*/

int 与 float 在内存中的存储方式不同,所以二者在对数据进行处理后展示的内容也是不同的;

浮点数的保存是不精确的,是会存在误差的;

应用-判断系统大小端

  • 小端系统:低位数据存储在低地址内存中
  • 大端系统:低位数据存储在高地址内存中

daxiaoduan

将 1 填入低位,余下补 0,这就是小端系统

将 1 填入高位,余下补 0,这就是大端系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>

int isLittleEndian() {
union {
int i;
char a[4];
} test = {0};

test.i = 1;

return(test.a[0] == 1);
}

void main() {
// 返回 1 说明是小端,返回 0 则是大端
printf("%d\n", isLittleEndian());
}

enum

  • enum 是 C 语言中的自定义类型关键字
  • enum 能够定义整形常量的集合类型
1
2
3
4
5
6
enum TypeName {
IntConst1,
INtConst2,
// ...
IntConstN
};

注意事项

  • 第一个枚举常量默认值为 0
  • 后续常量的值在前一起常量值的基础上加1
  • 可以任意对枚举常量指定整型值(只能指定整型值
1
2
3
enum Day {MON = 1, Tue, WED, THU, FRI, SAT, SUN}; // 1, 2, 3, ,4 ,5, 6, 7
enum Season {Spring, Summer = 3, Autumn, Winter = -1}; // 0, 3, 4, -1
// enum Day、num Season 大小分别为 4 个字节

在 C/C++ 中,枚举类型中的每个枚举成员(如 MON, Tue, 等等)是一个编译期常量。这些常量并不需要存储在枚举类型中,它们只是用来表示某些具体的数值,通常会被替换为它们的对应值,而不是占用存储空间。

这些常量在编译时是已知的,并且在运行时不需要存储在内存中。它们只是简单的整数常量,编译器在需要使用这些值时会直接替换它们