总目录 > 1 语言基础 > 1.2 C 语言数据类型
前言
从 1.1 C 语言基础 剥离出来的一部分,专门介绍数据类型以及函数等内容,同时大幅丰富了函数的知识。
子目录列表
1、基本数据类型
2、变量与常量
3、数据类型转换
4、数组
5、结构体
6、函数
1.2 C 语言数据类型
1、基本数据类型
程序执行的基本逻辑是输入数据 -> 处理数据 -> 输出结果,数据是程序在运算处理时最核心的对象。
数据被分为不同类型,称为数据类型,它是程序分配和使用存储单元的基本技术。下面介绍 C / C++ 中的几大基本数据类型:
① int 整型
整数类型,简称整型,占用 4 个字节,表示范围为 [-2 ^ 31, 2 ^ 31 - 1 ( = 2147483647)]。支持类型前缀限定符,如:
> short int - 短整型,占用 2 个字节,表示范围为 [-2 ^ 15, 2 ^ 15 - 1 ( = 32767)];
> unsigned int - 无符号整型,占用 4 个字节,表示范围为 [0, 2 ^ 32 - 1 ( = 4294967295)];
> long int - 长整型,完全同 int(在 32 位机器上);
> long long int - 超长整型,占用 8 个字节,表示范围为 [-2 ^ 63, 2 ^ 63 - 1 ( = 9223382036854775807)],可缩写为 long long(C++ 11 正式加入标准);
② float / double 浮点型
> float - 单精度浮点型,表示实数,占用 4 个字节,表示范围大致为 [-3.4 * 10 ^ 38, 3.4 * 10 ^ 38];
> double - 双精度浮点型,表示实数,占用 8 个字节,表示范围大致为 [-1.7 * 10 ^ 308, 1.7 * 10 ^ 308];
③ char 字符型
表示字符,占用 1 个字节,表示范围为 [0, 255];
另外还有 wchar_t,表示宽字符,占用 2 个字节;char16_t, char32_t,表示 Unicode 字符,分别占用 2, 4 个字节。
④ bool 布尔逻辑型
表示真(true, 1)或假(false, 0),占用 1 个字节,表示范围为 [0, 1];
⑤ void 无类型
int, short, char 前缀说明符缺省时,默认为 signed 类型,即有符号型。double 同样支持 long 前缀。
在 16 / 32 位的机器上,int 的长度分别为 2, 4 字节,如果需要提高可移植性,尽可能用 short / long 等前缀进行限定。
除了上述基本数据类型,还有诸多更为复杂的类型,下面会进行介绍。
2、变量与常量
① 变量
变量是指没有固定值而可以被改变的一类数据。每个变量都有它特定的类型,比如上述几大基本数据类型,其名称由字母、数字和下划线字符组成,且必须以字母和下划线开头,在计算机语言中是最关键的一类数据。
声明变量的一般格式:
变量数据类型 变量名, ...
比如:
int a; // 声明一个 int 类型的变量 a double b, c; // 声明两个 double 类型的变量 b, c
变量名只是程序可操作的存储区的名称,其可以存储所属数据类型对应的内容,比如:
int a; a = 5; // 将整数 5 赋值给 a
或者:
char d = 'a'; // 声明一个 char 类型的变量 d,且赋值为 'a'
声明的同时可以赋值。
同样地,变量除了可以是基本数据类型,也可以是复杂的数据类型,下面会进行介绍。
② 常量
常量是与变量相对的一类数,它必须在声明时被赋值,并且赋值后其值不能在被改变。
声明常量的一般格式:
const 常量数据类型 常量名 = 常量值;
比如:
const int a = 2; const char b = 'A';
3、数据类型转换
类型转换指将一种数据类型转换为另一种数据类型,分为隐式类型转换和显式类型转换。
① 隐式类型转换
如下 4 种情况,编译器会自动对参与运算的数据类型进行恰当地转换而无需程序猿参与,这样的类型转换称为隐式类型转换。
> 在同一个算术表达式中,如果出现了一种以上的不同数据类型,编译器会自行进行类型转换,再进行计算。转换原则是尽可能避免损失精度,由窄数据类型(占用存储空间小的类型)向宽数据类型转换,具体情况大致如下:
bool -> char -> short -> (unsigned)int -> long -> long long -> float -> double -> long double
> 将一种类型的数据复制给不同类型的变量,会发生隐式类型转换,将右值结果转换为左值类型。比如:
int a = 2; double b = 3.4, c = 2.2; b = a; // 将 int 类型变量 a 转换为 double 类型,值为 2.0,并赋值给 b a = c; // 将 double 类型变量 c 转换为 int 类型,精度损失,值为 2,并赋值给 a
> 函数调用时,如果实参表达式与形参类型不同,则将实参类型转换为形参;
> 函数返回时,如果返回表达式值与函数声明返回类型不同,则将结果转换为声明返回类型,比如:
double min(int a, int b) { return a < b ? a : b; }
由于形参 a, b 均为 int 类型,其返回表达式显然也是 int 类型,但声明返回类型为 double,则会将返回值从 int 转换为 double。
(关于这两条涉及到的函数会在下面介绍)
② 显式类型转换
显式类型转换也称为强制转换,指通过程序猿强行更改数据的类型,一般格式如下:
(强制转换类型) 变量 / 表达式名
举例:
int a = 1; double b = (double)a; // 将 int 变量 a 强制转换成 double,值为 1.0 ,赋值给 b a = (int)2.33; // 将浮点数 2.33 强制转换成 int,损失精度,值为 2,赋值给 a
③ 互通性
尽管我们提到 int 家族是用来表示整数,float / double 表示实数,char 表示字符,bool 表示真假逻辑,但 C / C++ 对这些类型之间的转换包容性极高,许多情况下可以直接相互转换,比如:
char x = 65; int y = 'a';
两者都是合法的,且数值是相等的。
bool a = 1; bool b = true;
两者也是合法的,且数值是相等的。
这一点,在比如 Java 等其他语言上要严格许多。C++ 的这种类型互通在方便编写的同时也降低了容错率。
4、数组
① 定义
前面已经介绍了变量,变量是独立的,即两个不同变量之间没有任何关系,而如果我们需要存储一系列有关联的变量,就要用到数组。
形如 a[x] 的变量声明为数组。a 是数组的名字,x 表示数组中元素的个数。可以和数学中的数列相联系。数组的类型可以为上述任何基本数据类型,比如一个 int 类型的数组变量 a[x],表示有 x 个连续 int 类型的变量,而不同于独立的变量,这 x 个变量并没有自己独一无二的变量名,所以在访问的时候是通过其所属数组的变量名 + 编号来访问。
对于数组 a[x],其编号为 [0, x),即数组中第 i 个元素的访问方式为 a[i - 1],i - 1 被称作该元素在该数组的下标。也就是说,第 1 个元素是存储在 a[0],这在一些情况下可能会使用不便,从日常习惯来看,一般 1 才是表示一切的开始,而 0 表示不存在,所以我们通常可以将数组 x 稍微定义较大一点,比如需要 100 个元素时,x 定义为 101(x + 1, x + 5, x + 10 均可,一般越大越能更好避免越界情况,但同时也会带来占用更多空间的问题),然后从 a[1] 开始存储 / 访问数据,方便理解。
声明的同时可以进行初始化,用花括号表示一个组。
下面给出一些数组的定义:
int a[10]; char c[5] = {'a', 'b', 'c', 'd', 'e'}; const double fk[105]; bool w[3] = {1, 1};
并不需要将所有元素均进行初始化,但也不能略过前面的元素而初始化后面的元素。
② 多维数组
上面介绍的数组属于一维数组,即只有一个下标,元素之间只有线性关系。那么,如果我们需要表示一个面甚至一个体的话,则需要用到多维数组。
二维数组由两个下标组成,形如 a[x][y]。数组总共包含 x * y 个元素,我们可以类比平面几何,将第一维 x 视作行,第二维 y 视作列,那么对于数组中的任意元素 a[i][j],可以理解为矩阵中第 i 行第 j 列元素的值。
三维数组同理,类比立体几何。当然,超越现实的更高维数组同样存在。
5、结构体
① 定义
相较于数组,结构体(struct)是一种更为灵活的高级数据类型。数组只能用来存储访问若干个相同数据类型的元素,而结构体,则可以将各类元素进行组合。先举个例子:
struct s { int b; double c; char d[10]; }; s a;
表示声明了一个名为 s 的结构体,这个结构体由一个 int 类型变量 b,一个 double 类型变量 c,一个 char 类型变量数组 d[10] 组成。接着,定义了一个名为 a 的结构体 s 变量。弄清楚 a 和 s 的区别 —— s 是结构体的名称,本身只是一种声明而不占用任何存储空间,是自定义的一种数据类型;a 是数据类型为 s 的一个变量,每定义一个这样的变量,就会同时生成一组 b, c, d[10]。对于任何存在的基本数据类型,都可以在结构体中进行定义。
同样地,结构体也可以是一组结构体,即结构体数组。比如:
struct s2 { int b; double c; char d[10]; } a[100];
a[100] 表示创建了一个数据类型为 s2,名为 a 的结构体数组,其中包含 100 个 s2 类型的结构体。
这里的定义方式和上面代码有一定区别,即直接在结构体作用域后写上变量名,再以分号结束,两种方式是等价的。
② 访问 / 修改元素
访问结构体中的各个变量有两种方式:
> 结构体变量名.成员元素名
比如上述代码,则可以使用诸如 a[1].b, a[2].c, a[3].d[5] 这种格式来访问元素。
> 指针名 -> 成员元素名 / (*指针名).成员元素名
关于指针,请参见 1.4.3 指针与引用。
修改的话同理,直接赋值即可。
③ 结构体与类
注意,这里描述的结构体是 C 语言中狭义的结构体,不包含 C++ 赋予的新特性。因为在 C++ 中,结构体和 C++ 中独有的类(class)极其相似,且 C++ 作为面向对象语言,其对类 / 结构体赋予的意义要更为生动,所以这一部分单独在 1.5.1 类与对象 中进行介绍。
④ 关于数组与结构体的声明位置
注意,数组和结构体往往占用空间大,所以一般情况下尽量定义为全局变量,因为如果定义为局部变量,则可能导致爆栈进而 RE。
加上上述的“局部变量不会赋初值 0”,我的 OI 生涯中几乎无一例外地将所有可以定义为全局变量的全部定义为全局了,是一种图方便的做法,可以很大程度上避免这种类型的 RE。其实从逻辑上看不符合代码规范,也不利于对程序的阅读和理解,所以以后还是尽可能的符合逻辑地定义各种变量。
6、函数
(原网站这么重要的部分竟然是空的,,所以我也时隔许久才更新这一块)
① 概念
最开始我们提到,每一个 C / C++ 程序都有个必不可少的部分:int main(),我们称之为主函数。那么,函数到底是什么?数学里我们其实已经接触许多,其实在计算机语言里也大同小异,从数学角度出发来介绍的话 —— 函数一般由自变量和因变量组成,最基础的函数表达式 y = f(x) 中,x 为自变量,y 为因变量,y 随着 x 的值变化而变化。而这个式子转化成 C 语言中是什么形式呢?举个例子:
int function(int x) { ... return y; }
这是一个名为 function 的 int 类型的函数。x 类似于自变量,称为参数,y 类似于因变量,称为返回值,其数据类型和函数的数据类型是一致的,即 int 类型,该类型称为函数的返回类型。返回类型可以是任何基本与高级数据类型。举个例子:
int max(int a, int b) { if (a > b) return a; else return b; }
这是求最大值函数。参数有两个 a, b,函数为 int 类型,很好理解。
② 函数原型与格式
上述函数是声明与定义同时进行,同样也可以先声明后定义。其中,函数声明部分又称为函数原型,由函数返回类型、函数名和形式参数表三部分构成,一般格式如下:
函数返回类型 函数名(形式参数 1, 形式参数 2, ...);
那上面的求最大值函数来说,int 为返回类型,max 为函数名,两个 int 变量 a, b 为形式参数,之间由逗号隔开。如果使用先声明后定义的方式,声明一般放在预处理器后,主函数前,而定义放在主函数后。
在这三部分中:
> 返回类型可以为空,用 void 来表示,不需要返回值。
> 形式参数可以缺省,即没有任何传递进去的值,但函数依然正常执行。
举个例子:
1 void add1() { 2 for (int i = 1; i <= n; i++) 3 ans += i; 4 } 5 6 int add2() { 7 for (int i = 1; i <= n; i++) 8 ans += i; 9 return ans; 10 } 11 12 int add3(int n) { 13 for (int i = 1; i <= n; i++) 14 ans += i; 15 return ans; 16 } 17 18 int main() { 19 int o = n; 20 add1(); 21 int a2 = add2(); 22 int a3 = add3(o); 23 return 0; 24 }
add3 函数有返回值,也有参数。我们将定义在主函数的局部变量 o 传递给 add3,且该参数命名为 n。这个 n 并不同于全局变量 n(尽管在这里它们值是相同的),也就是说在函数内访问 n 时,是默认访问函数的参数而非全局变量,这是个优先级的问题。然后我们将最后得到的 ans 返回到主函数定义的 a3 中。
add2 函数有返回值而没有参数。
add1 函数没有返回值,也没有参数。这样的函数一般是用来对程序段进行划分,使程序逻辑更加清晰。对于上述程序,由于 n 和 ans 都是全局变量,那么 add1 函数这样的结构已经能够完成任务了。
③ 函数参数调用
函数参数的调用有三种方式:传值调用(值传递),指针调用(指针传递),引用调用(引用传递)。
> 传值调用
传值调用指用实际参数的值来初始化形式参数,即将实参的值复制到对应形参中,复制完成后,实参与形参也就没有关系了,也就是说,形参的任何变化不会影响实参。函数执行结束后,用来保存形参的内存空间将被释放。以交换函数举个例子:
void swap1(int a, int b) { int t = a; a = b; b = t; } int x = 10, y = 5; swap1(x, y);
简单地说,这是个没有任何意义的函数。将实参 x, y 传递给 swap1() 的形参 a, b,函数对 a, b 进行值的交换,完全没有影响到实参 x, y。
> 指针调用
关于指针,请参见 1.4.3 指针与引用。
指针调用指用指针作为参数,即将实参的地址复制到对应形参中,复制完成后,形参的变化直接改变实参对应地址的值,也就是说,形参的所有变化会直接影响到实参。同样是交换函数举个例子:
void swap2(int *a, int *b) { int t = *a; *a = *b; *b = t; } int x = 10, y = 5; swap2(&x, &y);
调用 swap2() 后,x 的地址被复制到 a 对应的内存,y 的地址被复制到 b 对应的内存,再将两者地址指向的变量所保存的值对调,完成 x, y 的值的交换。
函数本身并不支持传递数组,但可以将数组作为参数传递给函数,而编译器会直接将其转换为该数组首元素的指针,因此,如下三种函数原型本质是相同的:
int a[100]; void sort(int *a); void sort(int a[]); void sort(int a[100]);
其中第三种虽然带上了元素个数,但并无法起到指定数组大小的作用,一般可同时传入一个表示数组大小的参数。
> 引用调用
引用是 C++ 的新特性,所以引用调用同样仅适用于 C++,但实在没必要单独开个专栏介绍引用调用了,所以这里一并提了。关于引用,请参见 1.4.3 指针与引用 中的 什么是引用 部分。
因为引用本质上也是一种指针,所以引用调用和指针调用区别同样不大,使用起来更方便,如交换函数:
void swap3(int &a, int &b) { int t = a; a = b; b = t; } int x = 10, y = 5; swap3(x, y);
引用作为参数传递的是实参变量本身(引用是变量左值,即实参的地址),但和指针调用一样会改变实参的值。
④ 默认参数
参数还可以指定默认值,即缺省参数。指定了默认值的参数不需要在调用时传值,但如果传值了,则默认值无效。
需要注意的是:在具有多个参数的函数中指定默认值时,所有默认参数都必须出现在非默认参数右侧,即一旦某个参数指定了默认值,其右侧所有参数全部需要指定。
举个例子:
int work(int o = 10, int p = 5) { ... } work(); work(1); work(1, 2);
第一次调用 work(),o = 10, p = 5;
第二次调用 work(1),o = 1, p = 5;
第三次调用 work(1, 2),o = 1, p = 2。
如果 o 设定了默认值 10,则 p 必须设定默认值,应该是出于传值会很麻烦的考虑。
⑤ 函数返回值
除了返回类型为空 void 的函数,所有函数都具有返回值,且函数都是通过返回语句结束函数调用的,也就是说,就算是 void 函数,系统也就在后面隐式执行返回语句。
返回语句一般格式为:
return 返回值;
在前面的求最大值函数中就已经很好体现了返回值的意义和使用方法。
⑥ 函数重载
函数重载指允许在同一作用域中定义多个形参列表不同的同名函数,以支持不同形式的函数调用。举个例子:
int abs(int o) { return o > 0 ? o : -o; } double abs(double o) { return o > 0 ? o : -o; }
系统会自动调用与实参类型最为匹配的那个重载函数。比如:
abs(9),9 最为匹配的是 int 类型,则调用第一个;
abs(-8.8),-8.8 最为匹配的是 double 类型,则调用第二个。
那么是根据什么原则自动调用的?
> 精准匹配:实参与函数形参类型完全一致,则直接调用,如上述两种;
> 提升匹配:实参需要从窄类型提升到宽类型才能有完全对应的重载函数,且不会出现精度损失,诸如 bool 到 char 到 int,float 到 double;
> 标准转换匹配:实参需要从窄类型提升到宽类型才能有完全对应的重载函数,但可能出现精度损失,诸如 double 到 int,double 到 long double 等;
上述两种即属于之前介绍的隐式类型转换的第三类。
> 自定义类型转换。
调用顺序优先级从前到后。
注意:重载函数必须具有不同参数表,而非不同返回类型即可;同时要避免出现二义性,即在调用时不满足精准匹配和提升匹配而只能标准转换匹配时,存在两个不同重载函数均可转换,比如:
int f(int o) { ... } long double f(long double o) { ... } double a = 2.33; f(a);
double 到 int 和 long double 均属于标准转换匹配,系统无法确定调用哪一个函数,即产生二义性。
⑦ 内联函数
在函数声明或定义时,将 inline 加到返回类型之前的函数是内联函数。其声明、定义和调用方法与非内联函数完全一致,区别在于编译器的执行方式。比如:
inline int max(int a, int b) { return a > b ? a : b; } int m1 = max(2, 3); int m2 = max(4, 5); int m3 = max(m1, m2);
对于内联函数,编译器会把函数调用语句替换成函数内的代码。听起来很抽象,以上述代码为例,其实际执行起来等价于:
int m1 = 2 > 3 ? 2 : 3; int m2 = 4 > 5 ? 4 : 5; int m3 = m1 > m2 ? m1 : m2;
也就是说,内联函数运行时不会进行参数传递,可以理解为更为高级的 define,所以执行效率更高;但与此同时程序代码会增加,存储空间占用更多。
注意,一般情况下只有非常简短且被经常调用的函数才适合作为内联函数,并且 inline 标识本身只相当于给编译器的一个建议,建议将该函数作为内联函数处理,而编译器会自行作出决定,所以并非加上 inline 就是内联函数。对于被递归调用的函数(关于递归,请参见 2.2 递归与分治),存在循环的函数或者代码量大的函数,不能作为内联函数。
⑧ 主函数
回过头来说主函数。这样是一种为了符合程序规范,自圆其说的设计,将整个程序打包成一个函数,并且最后需要写明其返回值为 0,所以程序在运行结束后可以看到终端会写上一句 “... with return value 0”(程序返回值为 0)。而如果它不是 0,则说明程序出现了问题,一般是 RE。