《C和指针》(Kenneth Reek)精读——第三章:数据

9 篇文章 17 订阅

描述变量的三个属性:作用域、链接属性和存储类型。这三个属性决定了变量的“可视性”(可以在声什么地方用)和“生命周期”。

3.1 基本数据类型

C语言的最基本的类型:整型、浮点型、指针和聚合类型(数组/结构体)

3.1.1 整型


整型包括:字符(char)、短整型(short int)、整型(int)、长整型(long int),他们分别为有符号(signed)和无符号(unsigned)
ANSI标准说明了各种整型值的最小范围,长整型至少应该和整型一样长(long intint),整型至少应该和短整型一样长(intshort int)

数据类型最小范围
char0到127
signed char-127到127
unsigned char0到255
short int 或 short-32767到32767
unsigned short int0到65535
int-32767到32767
unsigned int0到65535
long int 或 long-2147483647到2147483647
unsigned long0到4294967295

short intshort至少16位,long intlong至少32位,至于int是16或者32位由编译器设计者决定。C99和C++11标准引入了int_least16_tint_least32_tint_fast16_tint_fast32_t等类型定义,它们提供了更明确的最小位数和可能的性能优化。这些类型通常用于跨平台编程,以确保整数类型具有预期的最小位数。<limits.h>头文件提供了关于整数数据类型的各种限制和特性的信息<limits.h> 头文件:限制和特性

一、整型字面值

字面值(literal) 就是字面值常量,被初始化后他的值就不能再改变。

当一个程序内出现整型常量时,它是属于整型家族9种不同类型中的哪一种呢?为了明确指定字面值的类型,可以使用后缀:

  • Uu表示unsigned类型。
  • Ll表示long类型。
  • ULUluLul表示unsigned long类型。
  • LLll表示long long类型。
  • ULLUlluLLull表示unsigned long long类型。
int intValue = 123;          // 十进制int字面值
long longValue = 123L;       // 十进制long字面值
unsigned long ulongValue = 123UL; // 十进制unsigned long字面值

因为在没有后缀的情况下,小的整数字面量默认被解释为int类型。编译器可能会根据字面值的大小自动提升类型。例如,一个非常大的十进制字面值可能自动被视为long long类型,即使没有显式地使用LL后缀。

整数可以用十进制表示也可以用八进制和十六进制表示

  • 十进制字面值默认是int类型,如果数值超出了int的表示范围,则它可能是longlong long类型,这取决于编译器和平台。
  • 八进制字面值以0开头(例如0123),其类型规则和十进制字面值相同。
  • 十六进制字面值以0x0X开头(例如0x1A3F),其类型规则和十进制字面值相同。
int octalValue = 0123;       // 八进制int字面值
unsigned int hexValue = 0xABC; // 十六进制unsigned int字面值
unsigned long long hexLongLongValue = 0x123456789ABCLL; // 十六进制unsigned long long字面值

另外还有字符常量(character literal),尽管字符常量通常用于表示字符,但在内部它们实际上是整数值,表示字符在特定字符编码(如ASCII或Unicode)中的数值也就是int类型。因此不能给字符常量添加unsignedlong 后缀来改变其类型。

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为65

    enum Jar_Type { CUP=8,PINT = 16,QUART = 32,
    HALF_GALLON = 64,GALLON} ;
    

3.1.2 浮点型


浮点数常量必须有一个小数点或一个指数,浮点数家族包括floatdoublelong 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]); // 输出:W
    return 0;
}

在这个例子中,ptr是一个指向字符串常量的常量指针,而array是一个字符数组。我们通过strcpyptr指向的字符串复制到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,dint类型。

3.2.4 隐式声明


在C语言中,有几种声明的类型名在某些情况下可以省略,但这通常依赖于上下文或特定用法。以下是一些可以省略类型名的场景:

  1. 函数参数或返回值声明:函数如果不显式地声明返回值的类型,它就默认返回整型。当使用旧风格声明函数的形式参数时,如果省略了参数的类型,编译器就会默下认它们为整型。然而,这种做法在现代C语言编程中已经不推荐使用,因为它降低了代码的可读性和可维护性。

    func(a, b) { /* ... */ } // 旧的K&R风格,不推荐使用
    
  2. 结构体成员:在结构体的定义中,第一个成员的类型名有时可以省略,如果它的类型与上一个成员的类型相同。这通常用于实现某种形式的位域(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++等编程语言中都是用于定义别名或宏的工具,但它们之间存在一些重要的区别。

  1. 类型定义与宏定义

    • typedef用于为数据类型定义新的名称或别名。这有助于使代码更清晰,更易读。
    • #define是预处理器指令,它用于定义宏。宏可以是常量、表达式或语句。例如,#define PI 3.14159定义了一个名为PI的常量。
  2. 作用域

    • typedef有正常的作用域规则,如果你在函数内部定义了一个typedef,那么它只在该函数内部有效。
    • #define定义的宏没有作用域限制,除非使用#undef明确取消定义。因此,它们在整个源文件中都是可见的,甚至可能影响到其他包含此头文件的文件。
  3. 调试和类型检查

    • 由于typedef是语言级别的特性,因此编译器会对其进行类型检查。如果尝试将错误类型的值赋给typedef定义的变量,编译器会报错。
    • #define定义的宏在预处理阶段就被替换,因此编译器不会对其进行类型检查。这可能导致难以追踪的错误。
      #define d_ptr_to_char char *
      d_ptr_to_char a, b;
      
      正确地声明了a,但是b却被声明为一个字符。
  4. 参数化宏与函数

    • #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,隐藏了外部变量f
        printf("Inner f: %d\n", f); // 输出内部变量f的值
    }
    
    printf("Outer f: %d\n", f); // 输出外部变量f的值
    return 0;
}

为了避免混淆和错误,最好避免在不同作用域中使用相同的变量名,或者至少确保清楚地了解何时访问的是哪个变量。有些编程风格和约定也提倡使用前缀或后缀来区分不同作用域的变量,比如使用 inner_fouter_f 代替仅仅使用 f

3.5.2 文件作用域


任何在所有代码块之外声明的标识符都具有文件作用域(file scope),或称为全局作用域(global scope),它表示这些标识符从它们的声明之处直到它所在的源文件结尾处都是可以访问的。例如声明1和2

全局变量和函数在整个源文件中都是可见的,除非它们被其他同名的局部变量或函数所隐藏(shadow)。此外,如果全局变量或函数在另一个源文件中被声明为extern,它们也可以在那个源文件中被访问,前提是这两个源文件都被编译并链接在一起。

#include <stdio.h>

// 声明全局变量
int globalVar = 10;
void someFunction() {
    // 在这里可以访问globalVar
    printf("Global variable value: %d\n", globalVar);
}

int main() {
    // 在main函数中也可以访问globalVar
    printf("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.cfile2.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);
    }
    

    在这个例子中,externalVarfile1.c 中定义,并默认具有 external 链接属性。在 file2.c 中,使用 extern 关键字声明了 externalVar,表明它在另一个文件中定义,并且我们想要访问的是同一个变量。因此,在编译和链接这两个文件后,printExternalVar()printExternalVarInFile2() 函数将访问和打印同一个 externalVar 变量的值。

  • internal(内部)链接属性:属于internal链接属性的标识符在同一个源文件的所有声明都表示同一个实体,但位于不同源文件的多个声明则分属不同的实体。这意味着,如果在同一个源文件中多次声明了一个具有internal链接属性的标识符,它们实际上是指向同一个实体。然而,如果在另一个源文件中声明了相同的标识符,那么它将是一个完全不同的实体。
    external链接属性,在它前面加上 static关键字可以使它的链接属性变为internal。static只对缺省链接属性为external的声明才有改变链接属性的效果~
    假设我们仍然有两个源文件:file1.cfile2.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);
    }
    

    在这个例子中,internalVarfile1.c 中定义为 static,因此它具有 internal 链接属性。这意味着 internalVar 只对 file1.c 内的代码可见。在 file2.c 中尝试使用 extern 声明 internalVar 会导致编译错误,因为 internalVarfile2.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 ...
序号变量名 /函数名链接属性作用域存储类型说明
1aexternal全局1~ 12行和17 ~ 28行静态变量序号11的a一直到括号}暂时把这个a隐藏
2bexternal全局2~ 6行和25~ 28行静态变量序号7的b一直到函数d结束暂时把这个b隐藏
3cinternal全局3~ 28行静态变量-
4dexternal全局- (不是变量)
5enone原型5~11行、17 ~19行和23 ~24行auto变量
6fnone局部6~24行auto变量
7bnone局部7~24行register变量
8gnone局部静态变量
9a声明并不需要。这个代码块位于第1行声明的作用域之内
10enone局部12~16行auto变量
11anone局部13~16行auto变量
12hexternal全局(在定义它的文件中)静态变量
13xnone局部19~22行auto变量
14enone局部20~22行auto变量
15iinternal全局- (不是变量)

在这里插入图片描述
在这里插入图片描述

  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XiYang-DING

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值