本文主要是介绍《C和指针》(Kenneth Reek)精读——第三章:数据,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
第三章:数据
- 3.1 基本数据类型
- 3.1.1 整型
- 一、整型字面值
- 二、枚举类型
- 3.1.2 浮点型
- 3.1.3 指针
- 一、指针常量(pointer constant)
- 二、字符串常量(string literal)
- 3.2 基本声明
- 3.2.1 初始化
- 3.2.2 声明简单数组
- 3.2.3 声明指针
- 3.2.4 隐式声明
- 3.3 typedef
- 3.4 常量
- 3.5 作用域
- 3.5.1 代码块的作用域
- 3.5.2 文件作用域
- 3.5.3 原型作用域
- 3.5.4 函数作用域
- 3.6 链接属性
- 3.7 存储类型
- 3.8 static关键字
- 3.9 链接属性、存储类型和作用域示例
描述变量的三个属性:作用域、链接属性和存储类型。这三个属性决定了变量的“可视性”(可以在声什么地方用)和“生命周期”。
3.1 基本数据类型
C语言的最基本的类型:整型、浮点型、指针和聚合类型(数组/结构体)
3.1.1 整型
整型包括:字符(char
)、短整型(short int
)、整型(int
)、长整型(long int
),他们分别为有符号(signed
)和无符号(unsigned
)
ANSI标准说明了各种整型值的最小范围,长整型至少应该和整型一样长(long int
≥int
),整型至少应该和短整型一样长(int
≥short int
)
数据类型 | 最小范围 |
---|---|
char | 0到127 |
signed char | -127到127 |
unsigned char | 0到255 |
short int 或 short | -32767到32767 |
unsigned short int | 0到65535 |
int | -32767到32767 |
unsigned int | 0到65535 |
long int 或 long | -2147483647到2147483647 |
unsigned long | 0到4294967295 |
short int
或 short
至少16位,long int
或 long
至少32位,至于int
是16或者32位由编译器设计者决定。C99和C++11标准引入了int_least16_t
、int_least32_t
、int_fast16_t
、int_fast32_t
等类型定义,它们提供了更明确的最小位数和可能的性能优化。这些类型通常用于跨平台编程,以确保整数类型具有预期的最小位数。<limits.h>
头文件提供了关于整数数据类型的各种限制和特性的信息<limits.h> 头文件:限制和特性
一、整型字面值
字面值(literal) 就是字面值常量,被初始化后他的值就不能再改变。
当一个程序内出现整型常量时,它是属于整型家族9种不同类型中的哪一种呢?为了明确指定字面值的类型,可以使用后缀:
U
或u
表示unsigned
类型。L
或l
表示long
类型。UL
、Ul
、uL
或ul
表示unsigned long
类型。LL
或ll
表示long long
类型。ULL
、Ull
、uLL
或ull
表示unsigned long long
类型。
int intValue = 123; // 十进制int字面值
long longValue = 123L; // 十进制long字面值
unsigned long ulongValue = 123UL; // 十进制unsigned long字面值
因为在没有后缀的情况下,小的整数字面量默认被解释为int
类型。编译器可能会根据字面值的大小自动提升类型。例如,一个非常大的十进制字面值可能自动被视为long long
类型,即使没有显式地使用LL
后缀。
整数可以用十进制表示也可以用八进制和十六进制表示
- 十进制字面值默认是
int
类型,如果数值超出了int
的表示范围,则它可能是long
或long long
类型,这取决于编译器和平台。 - 八进制字面值以
0
开头(例如0123
),其类型规则和十进制字面值相同。 - 十六进制字面值以
0x
或0X
开头(例如0x1A3F
),其类型规则和十进制字面值相同。
int octalValue = 0123; // 八进制int字面值
unsigned int hexValue = 0xABC; // 十六进制unsigned int字面值
unsigned long long hexLongLongValue = 0x123456789ABCLL; // 十六进制unsigned long long字面值
另外还有字符常量(character literal),尽管字符常量通常用于表示字符,但在内部它们实际上是整数值,表示字符在特定字符编码(如ASCII或Unicode)中的数值也就是int
类型。因此不能给字符常量添加unsigned
或 long
后缀来改变其类型。
int main() { char ch = 'A'; // 字符常量赋值给char变量,发生隐式类型转换 int i = 'A'; // 字符常量直接赋值给int变量,没有类型转换 printf("Character constant as int: %d\n", 'A'); // 输出字符常量的整数值 printf("Character variable: %c\n", ch); // 输出字符变量的字符表示 printf("Integer variable: %d\n", i); // 输出整数变量的整数值 return 0;
}
在这个例子中,尽管 ch
被声明为 char
类型,但当你给它赋一个字符常量 ‘A’ 时,实际上是将字符常量(其内部是int
类型)的值隐式转换为char
类型。而变量 i 直接接收字符常量的 int
值,没有发生类型转换。
如果个多字节字符常量的前面有一个L
,那么它就是宽字符常量(wide character literal) ,宽字符是为了解决国际化,英文软件写好后,要发行到不同的国家,这时就需要使用宽字符,宽字符能把汉字当成一个字符。宽字符相关博客
二、枚举类型
枚举(enumerated) 类型就是指它的值为符号常量而不是字面值的类型。
//声明枚举类型变量
enum { CUP,PINT,QUART,HALF_GALLON,GALLON }
milkjug. gas_can, medicine bottle;
-
变量实际上以整型的方式存储,这些符号名的实际值都是整型值。
CUP
为0,PINT
为1,以此类推。 -
也可以指定符号名为特定的整型值,只对部分符号名用这种方式进行赋值也是合法的。如果某个符号名未显式指定一个值,那么它的值就比前面一个符号名的值大1。eg.
GALLON
为65enum Jar_Type { CUP=8,PINT = 16,QUART = 32, HALF_GALLON = 64,GALLON} ;
3.1.2 浮点型
浮点数常量必须有一个小数点或一个指数,浮点数家族包括float
、double
和 long double
类型。
3.14159
1E10
25.
.5
6.023e23
浮点数字面值在缺省情况下都是double
类型的,除非它的后面跟一个L或l
表示它是一个long double
类型的值,或者跟一个F或f
表示它是一个float
类型的值。
头文件float.h
定义了一些和浮点值的实现有关的某些特性的名字,例如浮点数所使用的基数、不同长度的浮点数的有效数字的位数等。
3.1.3 指针
变量的值存储于计算机的内存中,每个变量都占据一个特定的位置。每个内存位置都由地址唯一确定并引用,指针就是地址的另一个名字。
一、指针常量(pointer constant)
指针常量指的是指针本身的值为常量,即指针所指向的内存地址是不可改变的。定义指针常量的格式通常为 <数据类型> *const <指针变量名>
。一旦初始化后,这个指针就不能再指向其他地址了,但是它所指向的内容是可以改变的。
指针常量多用于固定指针的指向,不发生地址偏移。例如,数组名在C语言中实际上就等价于一个指向数组首元素的指针常量。
与指针常量相对的是常量指针。常量指针指向的内容是常量,即指针所指向的值不可以修改,但是指针自身的值(即指向的地址)是可以改变的。定义常量指针的格式通常为 <数据类型> const * <指针变量名>
。
还有一种结合了指针常量和常量指针的概念,称为指向常量的指针常量,或者常量指针常量。它的定义格式为 <数据类型> const *const <指针变量名>
。这种指针既不可以修改其指向的值,也不可以修改其本身的值(即指向的地址)。
二、字符串常量(string literal)
字符串常量是由一对双引号括起来的字符序列,如"Hello, World!"
。在内存中,字符串常量以连续存放字符的ASCII码值的形式存储,并且在最末尾会添加一个结束字符'\0'
。这个结束字符用于标识字符串的结束,使得程序能够知道字符串的长度。
原文这里说的很绕,贴个图
字符串常量会生成一个指向其第一个字符的常量指针。当我们使用字符串常量,如"Hello, World!"
时,这个字符串字面量会被存储在程序的只读数据段中,并且编译器会生成一个指向该字符串首字符的指针。这个指针是常量的,意味着我们不能改变这个指针的值以使其指向不同的内存地址。但是,如果我们将这个字符串常量的地址赋值给一个非const
的字符指针,我们仍然可以通过这个指针来访问字符串中的字符。然而,这样的做法是不安全的,因为尝试修改通过非const
字符指针访问的字符串常量中的字符会导致未定义行为,通常会导致程序崩溃。
与字符数组不同,字符数组是在栈上分配的,并且其内容是可变的。我们可以将字符串常量的内容复制到字符数组中,然后修改数组中的字符。这通常是通过strcpy
函数来完成的。
这里有一个例子来阐明这些概念:
#include <stdio.h>
#include <string.h>
int main() {const char *ptr = "Hello, World!"; // ptr 是一个指向字符串常量的常量指针char array[50]; // array 是一个字符数组// 将字符串常量的内容复制到字符数组中strcpy(array, ptr);// 现在可以通过数组修改内容array[7] = '!'; // 安全的,因为array的内容是可变的// 试图通过ptr修改内容是未定义行为// ptr[7] = '!'; // 错误的,因为ptr指向的内容是不可变的printf("%s\n", ptr); // 输出:Hello, World!printf("%s\n", array); // 输出:Hello, World!printf("%c\n", array[7]); // 输出:Wreturn 0;
}
在这个例子中,ptr
是一个指向字符串常量的常量指针,而array
是一个字符数组。我们通过strcpy
将ptr
指向的字符串复制到array
中,然后可以安全地修改array
中的字符。
3.2 基本声明
3.2.1 初始化
这块没什么好说的~,写是为了有一个整体学习的框架
int j= 15;//声明j为一个整型变量,其初始值为15
3.2.2 声明简单数组
int values[20]
名字values
加一个下标,产生一个类型为int
的值(共有20个整型值)。
- 下标从0开始
- 编译器并不检查程序对数组下标的引用是否在数组的合法范围之内。从技术上说,让编译器准确地检查下标值是否有效是做得到的,但这样做将带来极大的额外负担。
3.2.3 声明指针
int *a;//a被声明为类型为int*的指针
int* a;//c语言比较自由~也对
但是注意一下以下写法
int* b,c,d;
星号实际上是表达式b
的一部分,只对这个标识符有用。b
是一个指针,但c
,d
是int
类型。
3.2.4 隐式声明
在C语言中,有几种声明的类型名在某些情况下可以省略,但这通常依赖于上下文或特定用法。以下是一些可以省略类型名的场景:
-
函数参数或返回值声明:函数如果不显式地声明返回值的类型,它就默认返回整型。当使用旧风格声明函数的形式参数时,如果省略了参数的类型,编译器就会默下认它们为整型。然而,这种做法在现代C语言编程中已经不推荐使用,因为它降低了代码的可读性和可维护性。
func(a, b) { /* ... */ } // 旧的K&R风格,不推荐使用
-
结构体成员:在结构体的定义中,第一个成员的类型名有时可以省略,如果它的类型与上一个成员的类型相同。这通常用于实现某种形式的位域(bit-fields)。
struct {int a:4;:4; // 省略类型名,继承上一个成员的类型int c:4; } myStruct;
3.3 typedef
typedef
它允许你为各种数据类型定义新名字。
typedef char *ptr_to_char;//这个声明把标识符ptr to char作为指向字符的指针类型的新名字
ptr_to_char a;//声明a是一个指向字符的指针
typedef
和#define
在C和C++等编程语言中都是用于定义别名或宏的工具,但它们之间存在一些重要的区别。
-
类型定义与宏定义:
typedef
用于为数据类型定义新的名称或别名。这有助于使代码更清晰,更易读。#define
是预处理器指令,它用于定义宏。宏可以是常量、表达式或语句。例如,#define PI 3.14159
定义了一个名为PI的常量。
-
作用域:
typedef
有正常的作用域规则,如果你在函数内部定义了一个typedef
,那么它只在该函数内部有效。#define
定义的宏没有作用域限制,除非使用#undef
明确取消定义。因此,它们在整个源文件中都是可见的,甚至可能影响到其他包含此头文件的文件。
-
调试和类型检查:
- 由于
typedef
是语言级别的特性,因此编译器会对其进行类型检查。如果尝试将错误类型的值赋给typedef
定义的变量,编译器会报错。 #define
定义的宏在预处理阶段就被替换,因此编译器不会对其进行类型检查。这可能导致难以追踪的错误。
正确地声明了#define d_ptr_to_char char * d_ptr_to_char a, b;
a
,但是b
却被声明为一个字符。
- 由于
-
参数化宏与函数:
#define
还可以用于定义带参数的宏,这有时可以比函数调用更快(因为宏在预处理阶段就被替换,没有函数调用的开销)。但是,这也可能导致一些不易察觉的错误,特别是当宏参数在宏定义中多次出现时。typedef
不支持参数化。
3.4 常量
本块已经在3.1.3的一、指针常量(pointer constant)详细说明,这里不再赘述。
3.5 作用域
编译器可以确认4种不同类型的作用域——文件作用域、函数作用域、代码块作用域和原型作用域。标识符声明的位置决定它的作用域。图中的程序骨架说明了所有可能的位置。
3.5.1 代码块的作用域
位于一对花括号之间的所有语句称为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域(block scope),表示它们可以被这个代码块中的所有语句访问。例如6、7、9、10
当代码块嵌套时,内部代码块中的变量或标识符的作用域是受限的,它们只在定义它们的那个代码块内有效。当内部代码块中声明了一个与外部代码块同名的标识符时,内部代码块中的标识符会**隐藏(shadow)**或“屏蔽”外部的那个标识符。
#include <stdio.h>int main() {int f = 5; // 声明6: 外部变量f{int f = 10; // 声明9: 内部变量f,隐藏了外部变量fprintf("Inner f: %d\n", f); // 输出内部变量f的值}printf("Outer f: %d\n", f); // 输出外部变量f的值return 0;
}
为了避免混淆和错误,最好避免在不同作用域中使用相同的变量名,或者至少确保清楚地了解何时访问的是哪个变量。有些编程风格和约定也提倡使用前缀或后缀来区分不同作用域的变量,比如使用 inner_f
和 outer_f
代替仅仅使用 f
。
3.5.2 文件作用域
任何在所有代码块之外声明的标识符都具有文件作用域(file scope),或称为全局作用域(global scope),它表示这些标识符从它们的声明之处直到它所在的源文件结尾处都是可以访问的。例如声明1和2
全局变量和函数在整个源文件中都是可见的,除非它们被其他同名的局部变量或函数所隐藏(shadow)。此外,如果全局变量或函数在另一个源文件中被声明为extern
,它们也可以在那个源文件中被访问,前提是这两个源文件都被编译并链接在一起。
#include <stdio.h>// 声明全局变量
int globalVar = 10;
void someFunction() {// 在这里可以访问globalVarprintf("Global variable value: %d\n", globalVar);
}int main() {// 在main函数中也可以访问globalVarprintf("Global variable value in main: %d\n", globalVar);// 调用函数someFunction();return 0;
}
3.5.3 原型作用域
原型作用域(prototype scope) 只适用于在函数原型中声明的参数名,例如声明3和声明8。
3.5.4 函数作用域
函数作用域(function scope) ,只适用于语句标签(statement labels),语句标签用于goto
。
在C和C++中,语句标签是用来与goto
语句配合使用的,以便可以跳转到代码中的特定位置。这些标签定义在函数内部,并且只能在它们被声明的那个函数内部被引用。因此,可以说语句标签具有函数作用域。
#include <stdio.h>
void myFunction() {int x = 0;if (x == 0) {goto label1; // 跳转到label1}printf("This will not be printed.\n");label1: // 语句标签printf("This will be printed because of the goto statement.\n");
}int main() {myFunction(); // 调用函数return 0;
}
在这个例子中,label1
是一个语句标签,它位于myFunction
函数内部。当x
等于0时,goto label1;
语句会导致程序控制流跳转到label1:
标签处,从而跳过中间的printf
调用。
现代编程风格通常倾向于避免使用goto
,而是使用循环、条件语句和函数调用来控制程序流程。
3.6 链接属性
链接属性(linkage) 是标识符的一个重要特性,它决定了在不同文件中如何处理标识符。在C/C++中,链接属性主要有三种:external(外部)、internal(内部)和none(无)。
-
external(外部)链接属性:具有external链接属性的标识符无论声明多少次、位于几个源文件,都表示同一个实体。这意味着,如果在一个源文件中声明了一个具有external链接属性的全局变量或函数,并在另一个源文件中使用
extern
关键字引用它,那么这两个源文件实际上是在访问同一个变量或函数。
假设我们有两个源文件:file1.c
和file2.c
。//`file1.c` // 外部链接属性的全局变量 int externalVar = 10;void printExternalVar() {printf("Value of externalVar in file1.c: %d\n", externalVar); }
//`file2.c` // 声明外部链接属性的全局变量 extern int externalVar;void printExternalVarInFile2() {printf("Value of externalVar in file2.c: %d\n", externalVar); }
在这个例子中,
externalVar
在file1.c
中定义,并默认具有 external 链接属性。在file2.c
中,使用extern
关键字声明了externalVar
,表明它在另一个文件中定义,并且我们想要访问的是同一个变量。因此,在编译和链接这两个文件后,printExternalVar()
和printExternalVarInFile2()
函数将访问和打印同一个externalVar
变量的值。 -
internal(内部)链接属性:属于internal链接属性的标识符在同一个源文件的所有声明都表示同一个实体,但位于不同源文件的多个声明则分属不同的实体。这意味着,如果在同一个源文件中多次声明了一个具有internal链接属性的标识符,它们实际上是指向同一个实体。然而,如果在另一个源文件中声明了相同的标识符,那么它将是一个完全不同的实体。
external链接属性,在它前面加上static
关键字可以使它的链接属性变为internal。static
只对缺省链接属性为external的声明才有改变链接属性的效果~
假设我们仍然有两个源文件:file1.c
和file2.c
。//file1.c // 内部链接属性的静态全局变量 static int internalVar = 20;void printInternalVar() {printf("Value of internalVar in file1.c: %d\n", internalVar); }
//file2.c // 尝试声明内部链接属性的静态全局变量(这将导致错误) extern int internalVar; // 错误:internalVar 在 file2.c 中不可见void printInternalVarInFile2() {// 由于 internalVar 在 file2.c 中不可见,下面这行代码会导致编译错误printf("Value of internalVar in file2.c: %d\n", internalVar); }
在这个例子中,
internalVar
在file1.c
中定义为static
,因此它具有 internal 链接属性。这意味着internalVar
只对file1.c
内的代码可见。在file2.c
中尝试使用extern
声明internalVar
会导致编译错误,因为internalVar
在file2.c
中是不可见的。 -
none(无)链接属性:具有none链接属性的标识符的多个声明均表示不同实体。这适用于局部变量和函数参数,它们默认就具有 none 链接属性。这些标识符在每次声明时都被视为独立的个体,不具有全局可见性。
对external再做说明,当extern
关键字用于源文件中一个标识符的第1次声明时,它指定该标识符具有external链接属性。但是,如果它用于该标识符的第2次或以后的声明时,它并不会更改由第1次声明所指定的链接属性。声明4并不修改由声明1所指定的变量i
的链接属性。
3.7 存储类型
变量的存储类型(storage class) 是指存储变量值的内存类型。有三个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器。
-
自动(auto)变量:代码块内部声明的变量的缺省存储类型是自动的(automatic),也就是说它存储于堆栈中。在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开该代码块时,这些自动变量便自行销毁。如果该代码块被数次执行,例如一个函数被反复调用,这些自动变量每次都将重新创建。在代码块再次执行时,这些自动变量在堆栈中所占据的内存位置有可能和原先的位置相同,也可能不同。
- 自动变量的初始化需要更多的开销,因为当程序链接时还无法判断自动变量的存储位置。
-
静态(static)变量 :凡是在任何代码块之外声明的变量总是存储于静态内存中 。静态变量在程序运行之前创建,在程序的整个执行期间始终存在。它始终保持原先的值,除非给它赋一个不同的值或者程序结束。(具体可看第二章:基本概念,2.1.2执行)
对于在代码块内部声明的变量,如果给它加上关键字static
,可以使它的存储类型从自动变为静态。- 函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。
- 在静态变量的初始化中,可以把可执行程序文件想要初始化的值放在当程序执行时变量将会使用的位置。当可执行文件载入到内存时,这个已经保存了正确初始值的位置将赋值给那个变量。完成这个任务并不需要额外的时间,也不需要额外的指令,变量将会得到正确的值。如果不显式地指定其初始值,静态变量将初始化为0。
-
寄存器变量:关键字
register
可以用于自动变量的声明,提示它们应该存储于机器的硬件寄存器而不是内存中。通常,寄存器变量比存储于内存的变量访问起来效率更高。使用频率最高的那些变量声明为寄存器变量。
3.8 static关键字
-
函数定义或用于代码块之外的变量声明时,
static
关键字用于修改标识符的链接属性,从external 改为internal,但标识符的存储类型和作用域不受影响。用这种方式声明的函数或变量只能在声明它们的源文件中访问。// file1.c static int globalVar = 10; // globalVar 只在 file1.c 中可见 static void staticFunc() { /* ... */ } // staticFunc 只在 file1.c 中可见
-
代码块内部的变量声明时,
static
关键字用于修改变量的存储类型,从自动变量修改为静态变量,但变量的链接属性和作用域不受影响。void someFunction() { static int staticLocalVar = 0; // staticLocalVar 只在 someFunction 中可见,但保留其值 // ... staticLocalVar++; // 每次调用 someFunction 时,staticLocalVar 都会递增 }
3.9 链接属性、存储类型和作用域示例
1 int a = 5;//(1)
2 extern int b;//(2)
3 static int c;//(3)
4 int d( int e )//d(4) e(5)
5 {
6 int f = 15;//(6)
7 register int b;//(7)
8 static int g = 20;//(8)
9 extern int a;//(9)
10 ...
11 {
12 int e;//(10)
13 int a;//(11)
14 extern int h;//(12)
15 ...
16 }
17 ...
18 {
19 int x;//(13)
20 int e;//(14)
21 ...
22 }
23 ...
24}
25 static int i(){//(15)
26 ...
27 }
28 ...
序号 | 变量名 /函数名 | 链接属性 | 作用域 | 存储类型 | 说明 |
---|---|---|---|---|---|
1 | a | external | 全局1~ 12行和17 ~ 28行 | 静态变量 | 序号11的a一直到括号}暂时把这个a隐藏 |
2 | b | external | 全局2~ 6行和25~ 28行 | 静态变量 | 序号7的b一直到函数d结束暂时把这个b隐藏 |
3 | c | internal | 全局3~ 28行 | 静态变量 | - |
4 | d | external | 全局 | - (不是变量) | |
5 | e | none | 原型5~11行、17 ~19行和23 ~24行 | auto变量 | |
6 | f | none | 局部6~24行 | auto变量 | |
7 | b | none | 局部7~24行 | register变量 | |
8 | g | none | 局部 | 静态变量 | |
9 | a | 声明并不需要。这个代码块位于第1行声明的作用域之内 | |||
10 | e | none | 局部12~16行 | auto变量 | |
11 | a | none | 局部13~16行 | auto变量 | |
12 | h | external | 全局(在定义它的文件中) | 静态变量 | |
13 | x | none | 局部19~22行 | auto变量 | |
14 | e | none | 局部20~22行 | auto变量 | |
15 | i | internal | 全局 | - (不是变量) |
这篇关于《C和指针》(Kenneth Reek)精读——第三章:数据的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!