C语言类型、操作符和表达式

[来源] 达内    [编辑] 达内   [时间]2012-09-17

变量和常量是程序中基本的数据对象,声明一系列变量,然后声明他们的初始值,操作符用来确定如何处理他们,表达式则是将变量和常量组合起来产生新的值。对象的类型决定了可以对他进行什么样的运算和赋值。这一篇文章主要就是讲解这些内容

变量和常量是程序中基本的数据对象,声明一系列变量,然后声明他们的初始值,操作符用来确定如何处理他们,表达式则是将变量和常量组合起来产生新的值。对象的类型决定了可以对他进行什么样的运算和赋值。这一篇文章主要就是讲解这些内容。

变量名称

  变量名称只能包含字母和数字,第一个字符必须为字母,下划线_被当作字母看待,有时候他对较长的名字的可读性起到很好的作用。不要用下划线对变量名称开头,虽然很多标准库经常这样命名变量,但是我们仍然不推荐。变量名称的大写和小写是敏感的,因此x和X是两个不同的变量名称。传统的习惯是使用小写来表示变量名称,使用大写来表示符号常量。

  内部名称的至少前31位字符是有效的,对于函数名称和外部变量可能没有这么长,因为汇编程序和加载程序可能会使用这些外部名称,而语言本身无法控制加载和汇编程序的。对于外部名称,ANSI标准进保证前六个字符的唯一性,且不区分大小写。类似于if,esle,int,float,ets等是被保留的,不能使用这些作为变量城城,而且他们必须使用小写。

  最好的做法是选择能够表示变量含义的字符串作为变量的名称,这样不容易昏药,一般对内部变量使用短的名称,尤其是循环标识,对于外部名称使用较长的字符串。

数据类型和大小

  在C语言中只有四个基本的数据类型,char字符,保存一个字符;int整型;float浮点型和double双精度浮点型。另外还有一些针对基本类型的约束类型,如short int sh;long int counter,一般在声明的时候,都会把int省略掉。short和long与int类型相同,仅仅是长度不同,根据不同的机器同样的类型长度也会不同。每个编译器都会根据自己的硬件选择合适的长度,但是short不能比int长,int也不能比long长,int要比long短,一般情况下他们分别是16位,32位和64位。

  修饰符singed和unsigned可以修饰char或者integer,无符号数字总是正的,不管字符是有符号的还是无符号的,可打印的字符总是正的。

  修饰符long double指定了浮点型的扩展,同整型一样,浮点型的长度也是由机器决定的,float,double和long double可以表示一个,两个或者三个不同的长度。

  头文件limits.h和float.h包含了符号常量,描述了这个机器和编译器的有关这些数据的大小以及其他的一些属性

常量

  一个整型的常量类似于1234,是int类型,如果是一个长整型的话需要使用l或L作为后缀,例如123456789L,如果一个整型太大又没有使用L标识,那么将会自动当作长整型来处理。无符号常量使用u或U标识,如ul或者UL标识一个无符号长整型。

  浮点型常量可以写成小数型(123.4)也可以写成指数型(1e-2),他们的类型是双精度,除非使用了f或者F指定了是一个浮点型常量,l或者L比哦是一个长的双精度类型。

  整型除了使用十进制的方法声明也可以使用八进制和十六进制,使用0开头的标识八进制,0x开头的标识十六进制,例如31可以写成037的八进制标识方法,也可以写成0x1f的十六进制标识方法。而且八进制和十六进制也可以使用L和U来标识是否是长整型和无符号整型,例如0XFUL,是一个无符号的常量,值是15。

  一个字符常量也是一个整型,当写成字符的时候用单引号引起来,如‘x’。在机器的字符集里面,每个字符都对应着一个整数,例如在ASCII里面,字符‘0’对应着48而与整型0没有任何关系,我们使用单引号引起来字符的写法而不是直接使用数字表示是为了程序的可读性。字符可以像其他整型一样进行数学运算,通常被用来比较字符是否相等。

  另外一些特殊的字符可以通过反斜杠加一个字符来表示,看起来像是两个字符,其实代表着一个字符,例如换行符\n。另外我们还可以用任意的字节大小的位模式来表示一个字符,例如使用‘\ooo’中ooo表示一到三个八进制数字,或者‘\xhh’中hh标识一个或者多个十六进制数字,因此我们可以写出来下面的声明:

   #define VTAB '\013'   /* ASCII vertical tab */
   #define BELL '\007'   /* ASCII bell character */
   #define VTAB '\xb'   /* ASCII vertical tab */
   #define BELL '\x7'   /* ASCII bell character */

  完整的转移字符表如下:

\a 警报(响铃)符号 \\ 反斜杠
\b 退格 \? 问号
\f 换页 \' 单引号
\n 换行 \" 双引号
\r 回车 \ooo 八进制数
\t 水平缩进 \xhh 十六进制数
\v 垂直缩进    

  字符常量‘\0’代表值为0的字符,是空字符,写成‘\0’而不是数字0是为了强调他的字符特性,但是虽然如此,他的数值也是0。常量表达式就是只有常量的表达式,这个表达式通常是在编译的时候求值,而不是在运行的时候,并且可以出现在任何常量可以出现的位置,例如:

  #define MAXLINE 1000
  char line[MAXLINE+1
];    #define
 LEAP 1 /* in leap years */

  int days[31
+28+LEAP+31
+30+31
+30+31
+31+30
+31+30
+31];

  一个字符串常量就是一些列的字符或者空字符的组合,如“I am a string”或者“”。双引号不是字符串的一部分,仅仅是用来标识这个是字符串而已。字符串可以在运行的时候进行拼接,例如“hello,”“world”与“hello, world”是等价的,这对于长的字符串的使用是非常有 的。字符串实际上就是一个字符的数组,在存储的时候除了存储双引号内的字符串,还要在尾部添加一个空字符‘\0’。这意味着对字符串的长度没有限制,但是程序必须要扫描整个字符串来确定字符串的长度,在标准库中有一个函数strlen(s)用于返回参数字符串s的长度,这个长度不包括结束符‘\0’,下面是一个我们自己的版本:

   /*
 strlen:  return length of s */

   int strlen(char
 s[])    {        int
 i;         while
 (s[i] != '\0
')            
++i;        return
 i;    }

strlen以及其他的一些有关字符串的函数都在标准库的头文件<string.h>中。

< p style="margin: 5px auto; padding: 0px; text-indent: 0px; line-height: 19px; color: rgb(0, 0, 0); font-size: 13px; font-family: verdana, 'ms song', 宋体, Arial, 微软雅黑, Helvetica, sans-serif; font-style: normal; font-variant: normal; font-weight: normal; letter-spacing: normal; orphans: 2; text-align: left; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; background-color: rgb(254, 254, 242); ">   但是要注意的是虽然都只包含一个字符,但是使用双引号和单引号的意义完全不一样,例如‘x’标识一个字符,这个字符对应着一个整数,“x”标识一个字符串,这个字符串中只有一个元素,另外在内存中,这个字符串的结尾还有一个空字符\0‘。

  另外,在C语言中还有一种常量,枚举常量,一个枚举常量是一系列整型的列表,例如 enum boolean {NO,YES};枚举中第一个元素的值是0,下一个是1,然后依次递增,除非在某个位置指定了数值。指定了数值之后的元素将会从这个元素的值继续递增下去,看下面的例子:

   enum escapes { BELL = '
\a'
, BACKSPACE = '\b
', TAB = '
\t'
,                   NEWLINE = '
\n'
, VTAB = '\v
'

, RETURN = '\r
' };     
enum months { JAN = 1
, FEB, MAR, APR, MAY, JUN,                  JUL, AUG, SEP, OCT, NOV, DEC };                        
/* FEB = 2, MAR = 3, etc. 
*/

不同枚举的名称必须不同,但是同一个枚举内不同的名称可以有相同的值。枚举可以提供一种非常方便的方法将名称和值联系起来,是#define的一种替换方法,但是他的值可以自动生成。尽管可以声明enum类型的变量,但是编译器不检查菏泽中类型的变量中存储的值是否为该枚举的有效值,不过枚举变量提供这种检查,因此枚举比#define更具有优势。此外,调试程序可以以符号形式打印出枚举变量的值

声明

  所有的变量都必须在使用前进行声明,当然,有些声明也可以通过上下文隐士声明。声明包含类型,同一个类型可以一次性声明一个也可以一次性声明多个变量,例如int lower,upper,step。也可以如下声明

   int  lower;    
int  upper;    
int  step;    
char c;    
char line[1000];

  第二种声明占用更多的行,但是方便注释,同时对后续的修改也带来了方便。声明也可以包含对变量值的初始化,等号后面可以是常量也可以是表达式,例如:

   char  esc = '
\\'
;    int
   i = 0;    
int   limit = MAXLINE+1
;    float
 eps = 1.0e-5;

  如果一个变量不是自动变量,那么初始化只被执行一次,从概念上讲,是在程序开始执行之前进行,初始化必须是一个常量表达式。显示初始化自动变量会在每次调用函数或者程序块的时候进行,初始化可以是任何表达式。外部变量和静态变量默认初始化为0,自动变量如果没有显示初始化,那么将会拥有一个undefine的值。

  const修饰可以加在任何变量之前,用于表示这个便在在声明后不能进行更改。,const也可以放在任何参数前面,表示这个函数不能更改这个参数,例如:int strlen(const char[]);如果试图去改变这个参数的值,那么其结果将会根据程序的具体实现而不同。

数学运算符

  双元运算符有+,-,*,/,还有取余运算%,例如计算一个年份是不是闰年的方法如下:

   if ((year % 4
 == 0 && year % 
100

 != 0) || year % 400
 == 0)        printf(
"%d is a leap year\n
", year);    
else        printf(
"%d is not a leap year\n
", year);

%运算符不能用在浮点和双精度浮点数据类型上,在有负数的情况下,整数除法截取的方向以及取余运算结果的符号取决于具体及其的实现,这和处理上溢出和下溢出是一样的。

  二元运算符+和-比*,/和%的优先级低,而*,/和%的运算优先级又比单元的+和-底。运算的方向都是从左向右进行。运算符的优先级会在本篇的最后部分详细列出来。

关系和逻辑运算符

  关系运算符有> >= < <=,他们具有相同的运算优先级,比他们第一级的关系运算符有==和!=。关系运算符要比数学运算符的优先级低,因此i<lim -1的运算顺序是i<(lim-1)。

  逻辑运算符有&&和||。被&&和||连接的表达式从左到有进行计算,一旦结果能够确定,那么将会终止逻辑判断,很多程序员都会通过这个特性来优化代码的运行,例如:

   for (i=0
; i < lim-1
 && (c=getchar()) != '

\n'
 && c != EOF; ++i)        s[i] = c;

在继续循环之前要先判断是否数组内还有空间,如果没有空间,那么后面的判断将会没有意义,因此把i<lim-1放在首位。对于getchar和EOF判断的位置也是同样的道理,因为在判断EOF之前要首先调用getchar函数。

  &&比||的优先级高,他们两个的优先级都比关系运算和相等运算优先级低,因此下面的表达式

 i < lim-1
 && (c=getchar()) != '
\n'

 && c != EOF

中,不需要使用多余的括号,因为!=的优先级比复制的优先级高,因此需要(c=getchar())!='\n'。根据定义,关系运算和逻辑表达式中,如果结果为真,那么表达式的结果为1,如果为假,那么表达式的值为0。单元运算符!可以将非零的操作数转换为0,将0转换为1,因此下面两种写法是一样:if (!valid)和if (valid == 0),很难说那种写法更好。类似于!valid的写法更加直观,如果不是有效的,但是如果是在更加复杂的语句下可能就会难以理解了。

类型转换

  当操作数的类型不同意的时候,会自动根据小数规则转化成同一个类型的数据进行计算,通常是将窄的数据转换成宽的数据,这样可以防止数据信息的丢失,例如f+i的计算会将整型转化为浮点型进行计算,但是也有一些表达式是不符合规则的,例如使用浮点型作为下标。会损失数据信息的表达式,例如将长整型转化为短整型,或者将浮点型转化为整型,会发出警告,但是并不是非法的。

  一个字符是一个较小的整型,因此字符可以自由地放在算术表达式中,这样为字符的转换带来了很大的灵活性,下面是一个函数,将一串数字转换为相应的数值

   /*
 atoi:  convert s to integer */

   int atoi(char
 s[])    {        int
 i, n;        n = 0
;        for
 (i = 0; s[i] >= '
0'
 && s[i] <= '9
'; ++i)            n 
= 10 * n + (s[i] - '
0'
);        

return n;    }

  s[i]-1是一个典型的计算数字字符数值含义的表达式,因为0-9的字符的数值是连续的。另外一个函数是将单词中的大写转换成小写,当然是在ASCII码的情况下:

   /*
 lower:  convert c to lower case; ASCII only */

   int lower(int
 c)    {        if
 (c >= 'A
' && c <= '
Z'
)            return
 c + 'a
' - '
A

';        
else

           return c;    }

  在ASCII码总A-Z字符对应的数字也是连续的。在标准头文件ctyp.h中,定义了一系列和字符集无关的测试和转换函数,例如tolower是一个类似于上面函数的版本,同样c>='0'&&c<='9'可以被isdigit(c)来代替。从现在开始我们将会开始更多的使用<ctypoe.h>中的函数。

  这里要注意的是,当字符和数字时间进行转换的时候字符并没有标记是由符号的还是无符号的,当一个字符转换为一个整型的时候,有没有可能是一个负数呢?这个根据机器和架构的不同而不同了。一些机器上一个字符的最左边一位为1的时候被转换成负数,另外一些机器上,一个字符在转换为整型的 候会在最左边添加一位为0的位,因此这个整型总是正的。在C语言中,任何可打印的字符都不会被转换为非负整数。但是一些字符可能会在一些机子上是负数,一些机器上是整数。为了程序的可移植性,,如果要在char类型中存储一些非字符数据的话,最好指定数据是有符号的或者无符号的。

  关系运算表达式i>j和用&&和||连接的逻辑表达式,如果值为1那么为true,如果值为0,那么为false,当然一些函数例如isdigit也可能会返回一些非零值来表示真,在while,for等循环中,true表示非零,这并不会造成太大的不同。

  在数学运算中的隐士类型转换还是一样的,在类似于+或者*的运算符中的操作数如果是不同的类型,那么低等类型会被转化为复杂一点的类型然后开始运算。运算结果为较高的类型。在一些没有使用无符号数的运算中,一般遵守如下的规则:如果有long double类型,那么转换为long double;否则如果有double类 型,则转换为double类型;否则,如果有float类型,按么转换为float类型;否则转换char和short为int类型;最后,如果有long则转换为long类型。最后注意flot类型不会自动转换为double,这和当初的定义有一些不同。通常,<math.h>中的数学函数会使用double类型,因此flaot类型在大数组中可以节省空间,同时在运行的时候float比double更加节省时间。

  如果存在无符号类型的话,那么转换规则就比较复杂了,因为不同机器的有符号和无符号类型的比较不一样,因为他们依赖于整型的大小。例如:如果机器的整型是16位,长整型是32位,那么-1L<1U,因为1U会被转换为一个有符号的long型。但是-1L>1UL,因为-1L被转换为无符号的long型,因而成一个比较大的long型。

  在赋值的时候也会发生类型转换,右边的数值会转换为左边的类型。上面还提到过,一个字符会转换为一个整型而不管是否是有符号位的。当将一个长的类型转换为一个较短的类型的时候,多出的部分将会被省略掉,如

   int  i;    
char c;     i 
= c;    c = i;

  c的值没有改变,不管有没有进行符号扩展,c的值都没有变,但是如果调换复制语句的顺序,那么就有可能发生信息丢失。如果x是浮点型,i是整型,那么x=i和i=x都会发生类型转换,浮点型到整型会忽略掉任何小数部分,当double转换为float时候,是进行四舍五入还是舍去需要根据具体的实现。

  因为函数调用也属于表达式,因此在传递参数的时候也有可能发生类型转换,如果没有函数原型声明的话,char和short转换为int,float转换为double,这也是我们我唉即使参数类型为char和float的情况下也会把原型声明为int和double类型的原因。

  最后,任何表达式中都可以进行显示的类型转换,使用cast的单元运算符。cast运算会根据上面的规则对类型进行转换。表达式首先将数据转换为指定的类型的变量,然后用这个变量来代替整个表达式。,例如在函数sqrt函数中需要一个double类型,如果传递的是其他的数据类型,函数将无法执行,因此,如果你有一个整型的时候,可以如下使用sqrt((double)n),来将整型转换为double,然后在赋值给sqrt。cast运算仅仅是生成一个指定类型的n的值,n本身并不会发生改变。cast运算符与其他单元运算符一样拥有相同的运算优先等级。

  如果已经声明了函数原型(通常我们都应该进行函数原型声明),那么将会对传入函数的任何类型的参数进行自动的类型转换,例如声明函数原型double sqrt(double)然后调用root2=sqrt(2),那么不需要强制类型转换,整型2会被自动转换为double类型。

  标准库中包含一个可移植性的生成随机数的函数和一个生成种子的函数,第一个函数中包含了强制类型转化,如下:

   unsigned long int
 next = 1;     
/*
 rand:  return pseudo-random integer on 0..32767 

*/

   int rand(void
)    {        next = next * 
1103515245

 + 12345;        
return (unsigned int
)(next/65536) % 32768
;    }     /*
 srand:  set seed for rand() 
*/   void srand(unsigned 
int

 seed)    {        next =
 seed;    }

增量和减量运算符

  C语言提供了两个特殊的自增和自减运算符,++和--,自增运算符有两种使用方法,一种是放在操作数的前面,一种是放在操作数的后面。放在操作数的前面表示先对操作数自增然后再使用操作数,放在后面表示先使用操作数,然后再去对操作数进行自增。也就是说在操作数需要被使用的上下文中++n和n++不一样。例如n的值是5,那么x=n++中x的值是5,而x=++n中x的值是6。这两种情况下n的值最后都是6,而且自增和自减只能针对一个操作数进行运算,(i+j)++是非法的。

  再看下面的一个例子,删除数组中出现指定的字符:

   /*
 squeeze:  delete all c from s */

   void squeeze(char
 s[], int c)    {       
int i, j;        
for (i = j = 0
; s[i] != '\0
'; i++)           
if (s[i] != c)               s[j

++] = s[i];       s[j] = '
\0'
;    }

  最后再看一个例子,函数strcat(s,t),将字符串t附加到字符串s的尾部,strcat假设s有足够的空间放置附加上的数组。我们书写的版本中没有返回值,但是标准库中返回一个结果字符串的指针:

   /*
 strcat:  concatenate t to end of s; s must be big enough */

   void strcat(char
 s[], char t[])    {        
int i, j;         i 
= j = 0;        
while (s[i] != '
\0'
) /* find end of s 
*/            i
++;        while
 ((s[i++] = t[j++]) != '\0


'); /* copy t 
*/    }

位运算符

  C语言提供了六个位运算符号,他们只能对整型进行运算,包括char,short,int和long不管他们是有符号的还是无符号的。&按位与,|按位或,^按位异或,<<左移,>>右移,~按位求反(一元运算符)。按位与常用语将某些位设置为0,例如n=n&0177将n中除了低7位的其他位设置为0;按位或常用语将对应的位设置为1,例如x=x|SET_ON将x中对应于SET_ON中的位设置为1;按位异或将对应的位进行判断,如果相同则为0,不同则为1。我们必须对按位或和按位与与逻辑运算或和与区分开来。

  位移操作<<和>>将操作数按位向左或向右唯一,向右位移操作的数必须是非负数。x<<2将x按位向左移动两位,多出来的位用0填充,相当于将x乘以4,右移一个无符号数也是将多出来的位补0,向右位移有符号数的时候有些机器采用符号位补充(即算术位移),有些机器采用0进行补充。

  一元运算符~用于求整数的二进制反码,即分别将操作数各二进制位上的1变为0,0变为1。例如x=x&~077将x的最后六位设置为0,注意,上卖弄的表达式与机器字长无关,假定x是16位的数值,它比形式为x&0177700的表达式要好,这种可移植性没有额外的开销,因为~0777是一个常量表达式,可以在编译的时候求值。

  最后我们来看一个例子,函数getbits(x,p,n)返回x中从位置p开始的持续n个字长的字段,起始是从右边开始数的。我们假设n和p都是合理的正值,例如getbits(x,4,3)返回第4,3,2三位的值。

   /*
 getbits:  get n bits from position p */


    unsigned getbits(unsigned x, int
 p, int

 n)    {        return
 (x >> (p+1-n)) & ~(~
0

 << n);    }

赋值运算符和表达式  

  表达式i=i+2可以写成i+=2。+=也是赋值操作符,任何一个二元操作副都可以写成这种形式,例如expr1  op= expr2可以写成 expr1 = (expr1)  op (expr2 )。x*=y+1等价于x=x*(y+1)。例如,下面是一个函数,用来统计一个整型中值为1的位数:

   /*
 bitcount:  count 1 bits in x */

   int
 bitcount(unsigned x)    {        int
 b;         

for (b = 0
; x != 0; x >>= 1
)            if
 (x & 01)                b

++;        return
 b;    }

  使用赋值运算符除了看起来更加简洁之外,还更加符合人们的思维习惯,如yyval[yypv[p3+p4] + yypv[p1]] += 2,可以避免人们阅读两个长串的表达式,而实际上他们是相同的,而且赋值运算符还有助于编译器生成高效的代码。

条件表达式

  if else表达式也可以使用一个三元运算符“?:”来表示,形式如下expr1?expr2:expr3.如果expr1是非零或者为真,那么将会执行expr2,否则会执行expr3表达式。expr2与expr3只会执行一个。因此,如果想要将a和b中较大的一个值赋给z的话,可以写成下面的样子z=(a>b)?a:b。需要注意到是,这个条件赋值语句可以出现在任何赋值语句可以出现的地方,如果expr2和expr3是不同的类型,那么结果的类型将会根据上面提到的规则进行判断,例如(n>0)?f:n,那么不管n是不是整数,结果都将会是float类型。

  条件表达式中的第一个表达式两边的括号并不是必须的,这是因为条件运算符?:的优先级非常低,仅仅高于复制运算符,但是我们依然推荐使用圆括号,因为这可以使得表达式的条件部分更加容易阅读。条件赋值语句可以使得代码更加简洁,例如下面的语句将数组中的字符打印出来,每十个一行,每个字符之间用空格隔开,每行之间使用换行符隔开:

   for (i = 0
; i < n; i++)        printf(
"

%6d%c"
, a[i], (i%10==9
 || i==n-1) ? '
\n'
 : ' ');

运算等级

  下面的表格是对所有的C语言中的运算符的优先等级的一个排序:

运算符 结合性
() [] -> . 从左向右
! ~ ++ -- + - * ( type) sizeof 从右向左
* / %  从左向右
+ -  从左向右
<< >>  从左向右
< <= > >=  从左向右
== !=  从左向右
从左向右
从左向右
从左向右
&& 从左向右
||  从左向右
?:  从右向左
= += -= *= /= %= &= ^= |= <<= >>= 从右向左
, 从左向右

  上面的优先级排序是从高到低进行排列,同一行中的运算符具有相同的运算等级。同大多数的语言一样,C语言没有指定同一个等级的操作符的操作顺序,也就是在x=f()+g()中,f()可以在g()之前也可以在其之后。如果函数f或g改变了另一个函数所使用的变量,那么x的结果可能会依赖于这两个函数的计算顺序,为了保证特定的计算顺序,可以把中间结果保存在临时变量中。

  同样C语言也没有指定函数各个参数的求值顺序,因此下面的语句printf("%d %d\n", ++n, power(2, n));,在不同的编译器中可能会产生不同的结果,这取决于n的自增运算是在power调用之前还是之后,解决的办法是先写++n,然后在进行打印printf("%d %d\n", n, power(2, n))。

  函数调用,嵌套赋值语句和自增自减运算符都有可能产生副作用,也就是在对表达式求值的同时,修改了某些变量的值,在有副作用的表达式中,执行结果同表达式中的变量被修改的顺序之间存在着微妙的依赖关系,例如a[i]=i++所带来的问题就是数组下标的i到底是自增前的值还是自增后的值。这种情况 下的结果与编译器有关,因此可能会产生不同的结果。C语言对大多数这类问题没有作具体的规定,表达式何时会产生这种副作用将会由编译器决定,因为最佳的求职顺序同机器的结构有很大关系。ANSI C标准明确规定了所有对参数的副作用都必须在函数调用之前生效,但这对前面介绍的printf函数调用没有什么帮助。

  在任何一种编程语言中,如果代码的执行结果与求职顺序相关,那么这是一种很不好的代码风格。为了避免这种问题,在不知道这些问题在各种机器上是如何解决的,就最好不要尝试运用某种特殊的实现方式。

资源下载