重新学习 c 语言(3)- c语言特性(二)数据对象
(二) 数据对象
所以我们先来说说这个数据对象,数据对象大概包含一下特征.
(1) 属性
数据对象包含哪些属性?
-
名称
-
值
-
类型
-
长度(尺寸)
-
内存占用
还有吗?.
我这里不按照数据对象是常量,变量还是数据类型(typedef 关键词定义 pascal 中用 type定义,数据类型暂不考虑,她只有名称和类型,像是类型的代号),我也搞不清常量和变量的确切定义(依据编译后机器语言的处理形式?比如说常量不占用内存,比方立即数,立即数是编码在代码中的,浮点数又有区别,立即数故名思意,当时有效,过后无效了,也就是没有生存期,作用域也是一个点),其实似乎搞清楚也没有什么太大用处.通常我们都是通过对象(数据对象简称对象吧)名称访问对象的值,但是立即数没有名称,还有一种比如通过手动分配内存(如malloc 函数)的变量也没有名称,需要通过间接引用访问其值.
对象的值是根据对象类型做出的解释,当然一般值都有内存占用,内存占用的空间就是其类型的长度(尺寸),当然实际尺寸和宿主有关,我们不用考虑,姑且认为char占用一个字节(8bit).不同长度(尺寸)的对象类型肯定不同,相同长度(尺寸)的对象类型也可能不同,比如 long(认为是4个字节) 和float.
C语言基本数据类型非常简单(其实,计算机的数据原本就简单,都是二进制表示的数),依据计算机处理的不同就两种类型 整型和浮点型!
整型根据长度和有无符号表示的不同分为一下几种类型
类型 | 长度 | 备注 |
---|---|---|
unsigned char | 8位 | 无符号char |
unsigned short int | 一般位16位 | 无符号 int |
unsigned long int | 一般为32位 | 无符号 long |
unsigned int | 根据计算机字长,32位机是32位 | 无符号 int |
signed char | 8位 | 符号char |
signed short int | 一般位16位 | 符号 int |
signed long int | 一般为32位 | 符号 long |
signed int | 根据计算机字长,32位机是32位 | 符号 int |
long 和short 修饰int时 int可以省略
另外c99支持long long 用来表示64位整型.(其实在64位机器上int就是64位,c设计者为了编译器的效率没有规定各种类型必须的长度,只是说short不长于int,int不长于long,所以有可能short=int=long,由编译器决定长度对程序性能来讲是有很大优势的)
无符号整型比较简单就是2的n次幂 -1 最大可以表示2的n次幂个数(n是位长)
有符号整型采用二进制补码格式表示!
浮点类型 采用IEEE格式
根据长度分为 float (32位) double (64位) 还有IA32CPU体系上的long double (80位)
由基本数据类型可以组成复合数据类型,比如
- 数组:有若干个相同数据类型组成(基本数据类型或复合数据类型,比如二维数组其实是两个一维数组组成)
- 记录:(借用pascal的称呼,因为”结构”有数据结构的意思)有若干个不同数据类型.
- 联合:和记录一样只是组成元素基于相同的偏移开始(其实知道内存结构,数据类型就跟清楚了)
- 枚举 只是一种整型常量的表示方式,不是真正的类型.
最后一个有意思的类型是指针(pointer),指针是一种对内存占用(上面说过)的间接引用.其实指针不能算作一种数据类型(函数指针是引用函数的入口地址,先不讨论),指针其实就是汇编语言中的间接寻址. 看看一个指针的定义:
int *pSize;
所有指针变量占用的内存长度都一样(一般位字长,32体系中是32位),指针变量的值是一个整型(尺寸是字长),她表示一个地址,而这个地址存放的是一个数据对象.所以我们说指针的类型其实是地址存放数据对象的类型,我们在定义时候说明的,比如上面说明pSize变量中存放的地址(pSize的值)处为一个int型的数据对象.如此而已.没有类型的指针只能传递,不能操作的比如void *;
指针的设计是c语言的灵魂,但也是把双刃剑!除了指针存放数据对象的地址这一特性,还有就是编译器提供的指针运算为指针的使用提供了广泛的用途!
先看一个例子:
比如++运算对于指针来讲是什么意思呢? 对于一个整型
int a = 10;
int b=a++; //b =11
a++表达式求值结果还是一个整型的数据对象.
上面的pSize 一样
int *pNext=pSize++;
pSize++表达式求值结果还是一个整型指针(地址),但这个地址是多少呢?
c语言是这样定的:pNext的地址是pSize地址+int数据类型的长度(比如4个字节,记住,字节是cpu处理地址的原子单位)
对于数组
char str[]=”hello,world”;
str是一个char数组长度初始化为”hello,world”的长度12,字符11一个’\ 0’.
char *pStr = &str[0];
利用指针运算,比如 *(pStr+4) 和 str[4] 是一样的!
pStr+4意味着第0个字符地址+4个字节后的地址,这个地址上的值(类型是char已经说明).
简单总结一下指针的运算
-
指针与相同类型指针的赋值和比较(一般指针在同一个数组中比较有意义).
-
将指针赋值为0,以及指针与0 的比较运算(将指针赋值为空,以及判断指针是否为空)
-
指针与整数之间的加减运算(相当于地址向后或向前移动sizeof(指针类型)字节).
-
两个指针的减法(一般在同一个数组中有意义)
打住….
(2) 作用域
其实应该先说生存期.不过这两个概念在任何语言中都是有的.作用域其实是一个编译时期的概念. 作用域与数据对象的声明有关系:
先说说代码块,作用域的空间(生存期是时间特性,哈哈难以避免的时空).
c语言的代码块(为了简单不考虑#include情况了,其实#include是在预编译时将引用的代码展开在文件处,所以按源文件考虑是一样的.一般c语言的声明在.h文件中,后来的语言为了避免引用的命名冲突,使用了namespace).
一个源文件是最大的代码块(其实整个程序是最大的代码块),包含在里面的函数是一层代码块,函数内部包含
{ }的语句也是代码块. 小的代码块总是被大的代码块包含.代码块中包含的代码块我们暂且成为子代码块.
C语言(后来的标准)允许在任何位置声明变量(数据对象).
理解了代码块就理解作用域了.
作用域范围就是从声明开始之后的代码块中,包含子代码块.这里说的是声明,不是定义.
函数的局部变量定义就是声明.声明外部变量用关键字extern
比如:
void print(void)
int main(int argc,char *argv[])
{
if (argc>2)
{
extern a;
printf("%d\n",a);
}
//int b = a; //这里不属于a的作用域
print();
return 0;
}
int a =100; //定义在这里 这以后默认声明了 ,所以下面的子代码块中都是可见的
void print(void)
{
printf("%d\n",a);
}
如果是定义在另外一个文件中的外部变量(哈哈,函数都是外部的),我们能不能可见呢?
先看看生存期.
(3) 生存期
c语言数据对象的生存期 分为三种:
外部变量: 生存期由编译器和操作系统决定,一般来说生存期从定义开始到程序结束为止.
内部变量(c语言也叫auto,另外寄存器变量就不考虑了),一般在函数内部(函数的子代码块中的定义的变量也一样)从定义开始(其实是函数开始),到函数结束为止.
动态创建的数据对象只是那些比如通过malloc调用得到的内存块上的数据对象,生存期靠程序员决定(不错,给程序员很大的发挥空间,不过是把”双刃剑”,以至于java不允许干这个事情).
另外注意一个关键字 static
如果是外部变量static修饰限制了这个外部变量的作用域,只能在本文件中使用.
如果是内部变量static ,改变了这个变量的生存期(类似一个外部变量).
当然函数也是外部的,如果用 static修饰,也是改变了这个函数的作用域,只能在本文件中使用.
没有static修饰的外部变量其实作用域可以是整个程序.
另外c引入c++的const关键字只是说明该变量的只读特性.仅此而已.
复杂类型的定义与声明
数组 数组定义时必须指明长度: int arrCount[10];
如果定义时之间初始化,则可以不指明长度,使用初始化常量中的长度.
Int arrCount[]={1,2};
声明时不需要指定长度,如果指定的长度和定义的不符则运行出错(一般编译器不检查此类错误) extern b[];
记录
首先定义一个记录类型(是一种数据类型)
struct String
{
int len;
char *str;
};
//根据类型定义记录数据对象
struct String str;
// 或者同时定义数据类型和数据对象
struct String
{
int len;
char *str;
}str;
作为外部变量声明时 需要指明数据类型
extern String str;
另外数组在参数传递和返回时的表现和记录不同!在下面的函数和程序结构探讨; 先说点别的.
哈哈,似乎我漏掉了最常用的一种数据类型: 字符串.
在c语言中字符串不是语言的内建类型,或者说c语言没有对字符串内建支持(pascal/Delphi等对字符串提供了内建支持),对于编译语言,语言的内建支持其实就是编译器的支持(Delphi的AnsiString的实现非常巧妙,既有效率,使用又简单,但缺点也是明显的其中之一就是如果不了解编译器的实现原理,一旦出现问题,会让程序员不知所措,或许也是c语言没有内置字符串支持的原因).
先说说字符,字符就是整型,根据不同的编码标准得到不同的字符集比如单字节的ASCII编码(其实是7位编码),双字节的unicode编码,还有许多编码标准,目前在网络应用比较广泛的是utf-8编码(不固定字节长度,但在0-127之间的编码兼容ASCII).对于ASCII编码,c语言是内置支持的,比如 char a = ‘a’; 变量a中存放的其实是97.字符编码就是某种字符由哪个整型数表示.
字符串在c中就是字符数组.我们常说的以0 结尾的字符数组.而真正赋予这种”数据类型”字面含义的是操作字符串的库函数(在string.h中定义),所以使用c语言开发的同学会抱怨,简单的字符串相加都要调用函数实现.
如果这样岂不更好(c里面没有string 可以typedef……)
string a = “hello,”;
string b = “world”;
string c= a +b; // c为”hello,world”
很多其他语言就是这样的!比如c++ (c++使用运算符重载这种”高深”的特性,除了浪费开发者的脑力,不知道还有什么好处).
其实作为一个复合类型,赋值运算,相加运算都有他特定的含义.在c中典型的
char str[]=”hello,world”;
str其实是这样的,字符数组赋初值是编译器实现的(打住,如果有空一定要修炼计算机原理方面的知识…)
h | e | l | l | o | , | w | o | r | l | d | \0 |
---|
当然 string c = a + b 这种语法也需要编译器去实现.c语言其实可以自己编写string的实现(没有编译器支持,只能是靠函数了…)
下面可以看看delphi编译器(Delphi 7)实现的ansistring, 看上去大概是这样的:
struct String
{
long refcount; //在-8偏移处存放引用计数
long length; //在-4偏移处存放字符串长度
char *str; //与0结尾的字符串兼容
};
这样几乎解决了0结尾字符串的所有缺点.
引用计数和编译器的Copy On Write 使字符串一方面可以动态创建(随意变换长度),又减少了赋值次数.-4偏移的字符串长度可以方便的得到串长(因为许多字符串操作依赖字符串长度,0结尾的字符串需要遍历字符串得到长度),另外0结尾的字符串不能存放字符’\0’,ansiString就可以.最后字符串实际内容存放着0结尾的字符串,兼容了c的这种字符串形式. 不过AnsiString需要编译器做许多工作实现生存期自管理和Copy On Write 等功能,高级的字符串操作需要我们了解编译器的行为.
如果喜欢,我们也可以用c实现字节的字符串格式和相应的操作函数.
语言的数据对象是核心内容,但根本上依赖计算机原理,所以随着对计算机系统的深入,了解的也越透彻,看问题反而越简单.深奥的原理其实往往是简单并且美的.