“c和指针” 学习笔记 --章节三

基本数据类型
C语言中只有四种基本数据类型:整型、浮点型、指针和聚合类型

整型

整型包括字符、短整型、整型、长整型,听上去长整型一定比短整型要大,但这个假设实际上不一定正确。因为它们的规则实际上是这样的:
长整型至少和整型一样长,整型至少和短整型一样长
signed 和 unsigned 改变整型的表示范围,当可移植问题比较重要时,最佳妥协方案就是把存储 char 型变量的值限制在 signed char 和 unsigned char 的交集中;并且只有当 char 显式声明为 unsigned 或者 signed 时才能进行算术运算;

整型常量(字面值)
int a = 123;
123 就是一个字面值常量,指定了自身的值,并且不允许发生改变;(ANSI C 允许命名常量–声明为const 的变量,它被初始化之后,他的值不能再被改变);但是从技术上说 -123 不是常量,而是常量表达式,负号被解释为单目运算符而不是数值的一部分,但在实践中这个歧义性没有什么意义。
当程序中出现整型字面值,那么它属于整型类型的哪一种呢?缺省状态下,他是最短类型但能完整容纳这个值;非缺省状态下,我们可以通过在字符末端添加 L 或 l,使整数被解释为 long,字符 U 或 u 使数值被解释为 unsigned ,如果添加 ul 那么就会被解释为 unsigned long;
其实整数也可以用八进制和十六进制来表示
20 = 024(八进制) = 0x14 (十六进制)
字符常量
他没的类型总是 int,你不能在后面添加 unsigned 或者 long 后缀;字符常量就是用单引号包围起来的单个字符(或字符转义序列或三字母词)
‘a’ ‘\n’ ‘??=’
标准也允许类似 ‘abc’ 这种多字节字符常量,但是在不同环境中可能不一样,所以不鼓励使用
如果一个多字节字符常量前面有一个 L ,那么他就是宽字符常量
如 L’e^’ 当运行环境支持一种宽字符集时就可以使用它们。

当一个字面值用于确认一个字中某些特定位的位置时,那么将他写为 16 进制或者 8 进制
如果一个值被当作字符使用,那么把这个值表示为字符常量可以使值这个意义更为清晰。
value = value - 48;
value = value - \60;
value = value - ‘0’;
上面三个语句含义完全一样; (\60 八进制转义字符)

枚举类型
枚举类型就是指他的值为符号常量而不是字面值的类型,假设代码中有以下内容:

#include <stdio.h>

enum Jar_Type {
    CUP,
    PINT = 80,
    QUART
} my_jar1, my_jar2, my_jar3;

int main() 
{
    my_jar1 = CUP;
    my_jar2 = QUART;
    my_jar3 = 118;
    printf("my_jar1 = %d    my_jar2 = %d   my_jar3 = %d\0", my_jar1, my_jar2, my_jar3);
    return 0;
}
// 打印结果如下
// my_jar1 = 0    my_jar2 = 81   my_jar3 = 118

声明为枚举类型的变量实际上是整数类型,你可以给 my_jar 赋值PINT,你也可以赋值为枚举类型中没有提到的 118,但是要避免这样使用,因为这样会削弱他们的含义;

浮点型

诸如 3.14159 和 6.02*10^23 这样的数值无法使用整数进行存储;但是可以使用浮点数进行存储,它们通常以一个小数以及以某个假定数为基数的指数组成;
浮点数包括 float、double 和 long double 类型

浮点数在缺省状态下都是 double 类型的,除非后缀跟一个 L 或 l 表示long double,或者后缀 F 或 f,表示float

整型数据以补码的形式保存在内存中,浮点型数据以 IEEE 754标准进行存储
什么是 IEEE 754?
对于 float 来说,存储占用了 四字节 32位
第1位是符号位
第 2-9 位是指数(相对于 127 的偏移量)
第 10-32 位代表尾数
以 5.0 为例 实际存储的就是 0100 0000 1010 0000 0000 0000 0000 0000
(-1)^0 * 1.01 * 2^(129-127) = 5.0 (1.01 就是十进制的 1.25)

#include <stdio.h>
void printBits32(unsigned int num) {
    for (int i = 31; i >= 0; i--) {
        printf("%d", (num >> i) & 1); // 这里移位操作并不会影响原来的 num 
        if (i % 4 == 0) {
            printf(" ");  // 每四位分隔一下,便于阅读
        }
    }
    printf("\n");
}

int main() {
    // 打印 float 类型的二进制
    union {
        float f;
        unsigned int i;
    } floatUnion;

    floatUnion.f = 5.0;
    printf("Float 5.0 in binary: ");
    printBits32(floatUnion.i);
    return 0;
}

指针

变量存储在计算机内存中,每个内存位置都由地址唯一确定并引用,指针是地址的另一个名字罢了,指针变量就是其值为另外一个内存地址的变量;
指针常量
指针常量和非指针常量本质上是不同的;程序员无法事先知道某个特定变量存储到内存中哪个位置,因此,程序员通过 & 获取变量的地址而不是把他的地址写成字面值常量的形式; 诸如 0xff2044ec 这样的字面值存储的是什么变量程序员根本不清楚,因此,把指针常量表达为字面值的形式几乎没有用处(NULL 除外,可以用 0 表示);
字符串常量
本质上是一个字符数组,是一串以 NUL 结尾的字符,字符串内部不能有 NUL ;字符串常量可以是空的,但即使是空字符串,依然存在作为终止符的 NUL字节;程序在使用字符串常量会生成一个指向该字符串的常量指针(指向常量的指针),当一个字符串常量出现在一个表达式中时,使用的其实就是常量指针存储的地址;
ANSI C 修改一个字符串常量的效果是未定义的;实践中请避免这样做,如果需要修改字符串,请把它存储在数组中;(既是常量指针,理论上指向的数据应该是不可修改的,但为什么还是可修改的呢?实际上我在操作字符串常量的时候报了堆栈溢出,没有给我操作的机会 哈哈)

基本声明
变量声明的基本形式是: 说明符 + 声明表达式列表
说明符包含了一些关键字:描述了被声明标识符的基本类型、缺省的存储类型和作用域等;
(signed 一般只作用于 char,因为其他整型在缺省状态下都是有符号数,但是 char 是否是 signed 则由编译器决定)
unsigned short int a;
unsigned short a; 是等价的,关键字 int 可以省略;
书中本节只描述了最基本的声明方式

    int i;  // 声明一个整型变量 i
    char j, k, l;  // 声明三个字符型变量 j k l
    int x = 15;   // 声明一个整型变量 x,并将它初始化为 15
    int values[20];  // 声明一个整型数组,这个数组中包含 20 个元素

数组下标检查的经验法则:如果数组下标是已知的正确值计算得来,那么无需检测他的下标;如果用作下标的值根据某种方法从用户输入数据产生得来的,那么使用它之前必须进行检测,确保在有效范围内;

声明指针
在 c 语言声明中,先给出一个基本类型,紧随其后是一个标识符列表,这些标识符组成表达式用于产生基本类型变量;
例如 int a; a产生的结果类型是 int;
int
a 和 int a 实际上效果是一致的,但是 int a 实际上不是一个好技巧,如果你遇到下面这种情况
int
a, b, c; 这句代码是声明了 三个 int 型指针吗? 实际上不是的,* 是 *a 的一部分,所以这句话真正的意思是声明了 两个整型变量 b c 和一个整型指针变量 a;
声明指针变量时也可以进行初始化
char *message = “Hello World!!!”;
这条语句把 message 声明为一个指向字符的指针,并用字符串常量中第一个字符的地址对该指针进行初始化;
上面这个声明看上去是把 字符串常量的地址赋值给表达式 *message,但是实际上是赋值给 message 的,换句话说,前面一个声明相当于
char *message;
message = “Hello World!!!”;

隐式声明
C语言有几种声明,类型名可以忽略;
函数不声明返回值类型,就默认返回整型;
使用旧风格声明函数形参时,如果省略参数类型,编译器默认他们为整型;
如果编译器推断出一条语句实际上是一个声明,如果他缺少类型名,编译器会假定他为整型;
!!! 程序员不要过分依赖隐式声明,这会影响代码可读性

#include <stdio.h>
// 函数返回值缺省和形参类型缺省在我的环境下可以实现
add(a, b)
{
    return a + b;
}

int main() {
    int i = 1;  
    int j = 2; // 如果我直接写成   j = 2; 在我的环境下编译器会报错;
    int result = 0;
    result = add(i, j);
    printf("result = %d\n", result);

    return 0;
}

typedef
C 支持一种 typedef 机制,允许为各种数据类型定义名字;typedef 写法和普通的声明相同,只需要把 typedef 放在声明的前面;例如: char *ptr_char; 把 ptr_char 声明为一个指向字符的指针。在添加 typedef 之后,声明变成
typedef char *ptr_char; 这个声明把标识符 ptr_char 作为指向字符指针类型的新名字;
ptr_char a; 那么这个语句就是把 a 声明为一个 char * 指针。

我们应该使用 typedef 而不是 #define 来创建新类型名

#define ptr_char char *

int main()
{
	ptrchar ptr1, ptr2;
}

如果按上述方法声明 很明显得到的结果是这样的
char * ptr1, ptr2;
这与我们期待的结果是不同的

常量
ANSI C 允许声明常量,常量的样子和变量完全一样,只是它们的值不能修改;
我们可以用 const 来声明常量
int const a;
const int a;
这两条语句是等价的,把 a 声明为一个整数,并且他的值不能被修改;
由于 a 不能被修改,我们无法赋值给他,我们如何让他一开始就拥有一个值呢?
在声明时对它进行初始化:
const int a = 5;
在函数中声明为 const 的形参在函数被调用时得到实参的值

#include <stdio.h>

int add(int a, const int b)
{
    // b = 1; 非 const 变量在进入该函数后也变成了 const 
	return a + b;
}

int main()
{
	int a = 1, b = 2, c = 0;
	b = 1;
	c = add(a, b);
	printf("c = %d", c);
	return 0;
}

指针变量与 const 组合
int *ptr;
普通的整型指针
int const *ptr;
指针指向地址所保存的值不可更改
int * comst ptr;
指针指向地址不可更改
int const * const ptr;
指针指向地址不可更改,指向地址所保存的值也不可以修改

!!!当声明变量时,如果声明的变量不会被修改,我们应当使用 const 关键字,这种做法让阅读程序的人有更清晰的提现,同时如果修改这个变量时,编译器可以发现这个问题;
名字常量
#define MAX_ELEMENTS 50
为 50 这个值创建了名字常量 MAX_ELEMENTS,名字常量可以给数值起符号名,用名字常量定义数组长度和循环计数器时可以提高程序的可读性。

作用域

编译器可以确认四种类型作用域—文件作用域、函数作用域、代码块作用域和原型作用域;标识符声明的位置决定他们的作用域。
代码块作用域
一对花括号之间的所有语句称为一个代码块,任何代码块开始位置声明的标识符都具有代码块作用域,表示他可以被代码块中所有语句可访问,函数的形参在函数体内部也有代码块作用域, 那么也就是说下述这种情况会报错

int add(int a, const int b)
{
	int a = 6; // 变量 a 重定义,因为形参已经声明了 a
	return a + b;
}

如果内层代码块有一个标识符名字与外层代码块标识符同名,那么内层标识符将隐藏外层标识符;(我们应该避免在嵌套的代码块中出现相同的变量名,我们没有很好的理由使用这种技巧)

文件作用域
任何在所有代码块之外声明的标识符都具有文件作用域,表示这些标识符从他们声明之处直到源文件结尾都是可以访问的;在文件中定义的函数名也具有文件作用域,因为函数名不属于任何代码块;在头文件中编写并通过 #include 包含到其他文件中的声明就好像它们直接写在那些文件中一样,它们的作用域不局限于头文件的文件尾;

原型作用域
只适用于函数原型中声明的形参名;唯一可能出现冲突的情况就是同一个原型中不止一次使用同一个名字;

函数作用域
只适用于语句标签,goto 会用到语句标签;规则就一条 一个函数中所有语句标签必须唯一;

链接属性

当一个程序各个源文件被分别编译之后,所有目标文件和函数库中引用函数链接在一起形成可执行程序;然而 如果相同的标识符出现在不同的源文件,链接器该如何处理?
标识符的链接属性决定如何处理不同文件中出现的标识符,标识符的作用域和它的链接属性相关;链接属性一共有三种—external、internal、none;没有链接属性的标识符(none)被当做不同的实体;internal 属性的标识符在同一个源文件中指向同一个个体,不同源文件中则分属不同的实体;external 属性链接符则不论声明多少次,位于几个源文件都表示同一个实体;
链接属性实例:

typedef char *a;
int b;
int c(int d)
{
    int e;
    int f(int g);
    .........
}

在缺省状态下,b c f 的链接属性是external,其余标识符的链接属性是 none;属于文件作用域的声明在缺省情况下为 externa 属性;因此,如果另一个源文件包含了 b 的类似声明并调用了函数 c,它们实际访问的是这个源文件定义的实体;f 的链接属性是 external 是因为他是一个函数名,调用这个 f 实际上会链接到其他源文件定义的函数;
extern 和 static 可以修改标识符的链接属性;如果某个声明正常正常状况下具有 external 属性,加上 static 后可以把链接属性变成 internal;例

static int b;
static int c(int d)
{
	.....
}

这样标识符 b 和 c 就将为这个源文件所私有;
static 只对缺省连接属性为 external 的声明才有改变链接属性的效果;如果标识符是属性是 none ,同时加上了 static 关键字,那么它的效果完全不一样;
extern 可以为一个标识符指定 external 属性;除了实体的具体定义位置之外,在它的其它声明位置都使用 extern 关键字。

存储类型

三个地方可以存储变量:普通内存、运行时堆栈、寄存器
变量缺省的存储类型取决于他的声明位置;在任何代码块之外声明的变量总是存储在静态内存中,这类变量称为静态(static)变量;静态变量在程序运行之前创建,在程序整个执行期间存在,如果不显式指定静态变量的值,静态变量将初始化为 0;
代码块内部声明的变量存储类型是自动的,存储于堆栈中,称为 auto 变量;关键字 auto 就是声明这种类型的,但是他极少使用,因为默认就是这种类型;在一个代码块中,程序执行到声明变量时,该变量才会被创建,该代码块执行结束时,变量被销毁;
代码块内部声明的变量,如果给他加上static,可以使他的存储类型由自动变为静态,但不能改变它的作用域;案例如下:

#include <stdio.h>

int main () {
    int i = 1;
    while ( i++ < 10) {
        static int a = 1;
        // int a;
        // printf("a = %d\n", a);

        if( 2 == i ) {
            a = 1;
        } else {
            a++;
        }
        printf("a = %d\n", a);
    }
    return 0;
}  

案例中每一次循环都会执行
static int a = 1;
但是实际上整段程序只声明了 一次 a;
他的打印结果如下
a = 1
a = 2
a = 3
a = 4
a = 5
a = 6
a = 7
a = 8
a = 9
register: 关键字提示编译器将声明变量保存在硬件寄存器中,提高运行效率,但是硬件寄存器是有限的,编译器不一定会理睬这一操作。
来源 c 和 指针

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值