一、前言
与基本类型相对应的是复合类型,基本数据类型一般只用于存储简单的数据,比如一个整数、小数、字符之类。
而很多时候我们却需要存储更加复杂的类型,比如一系列订单价格、或者一个学生的身份信息。
这些使用基本类型虽然也能完成,但却无法很好的表达出我们的意思,而复合类型的作用就在于此。
所谓的复合,本质上也是在前面基本数据类型上整合出的一套类型而已,不用它也行,但用它却能节省我们大量的工作。
二、数组
首先第一个要聊的是数组,它的作用是复合多个同类型数据,比如100
个int
类型的变量来存储一系列订单价格,用数组就会非常的方便:
int prices[100];
这可比自己一个一个去定义100
个int
类型变量快多了。
它由三部分组成:
- 每个元素的类型,也就是前面的
int
。 - 数组名,也就是这里的
prices
。 - 数组中元素个数,也就是后面的
100
,但要注意是需要用[]
来设置。
typeName arrayName[ArraySize];
特别注意这里的ArraySize
,也就是数组大小,必须为整形常数,比如这里写的100
,而不能写带有变量的式子,比如:
int a=10;
int nums[a*10];
这种就是不行的,因为这种数组声明必须要在编译的时候就知道你要多大,上面的代码想要使用,可以使用const
关键字,告诉编译器这是常量,以后不会被改变了,那也可以:
const int a=10;
//a=20 错误,常量无法被更改
int nums[a*10];
const
关键字很简单,放在变量声明语句前面即可,这样就无法更改它的值了。
而想要访问数组中的元素,则可以采用下面的方式:
int nums[10]; //10个元素
nums[0]=10; //第一个元素赋值为10
nums[9]=100; // 最后一个元素赋值为100
int tmp=nums[9]; //取出最后一个元素,赋值给tmp变量
使用的依旧是[]
,不同之处在于我们现在的目的是进行索引,并且索引下标要从0
开始。
至于其它的操作,那就和普通的变量一样使用即可,正常的赋值、取值。
同时要注意,编译器并不会检查你下标的有效性,所以下面这样的代码依旧可以通过编译并运行起来:
int nums[10]; //10个元素
nums[10]=100; //虽然超出了数组索引范围0-9,但编译时并不会报错
这种越界使用数组的行为就极易造成运行时错误。
由于vs默认开启了缓冲区溢出安全检查,所以在vs里面上面这样的代码编译不会通过。
不管怎么说,使用数组的时候都一定要记住:索引是从0开始的。
更多细节可以参考文章:数组。
三、字符串
本质上来说,字符串其实也是数组的一种,不同之处在于它存储的是一系列字符而已。
#include<iostream>
int main() {
char num[10];
num[0] = 'h';
num[1] = 'e';
num[2] = 'l';
num[3] = 'l';
num[4] = 'o';
num[5] = '\0';
std::cout << num;
}
并且尤其注意,字符串必须要用0
作为结尾,就像上面这样。
但这样使用的话也太麻烦了,所以可以将其简写为:
char num[10] = { 'h','e','l','l','o','\0' };
std::cout << num;
事实上,前面的数组也可以这样进行初始化,直接让其等于一个{}
,然后在{}
里面挨个填值即可。
但这样写你仍需要注意最后那个0
作为结尾,没有0
作为结尾的字符数组在C/C++中不能被当作字符串使用!
上面这样写还是很麻烦,所以还可以更进一步简化:
char num[10] = "hello";
std::cout << num;
由于在C/C++中被""
包裹起来的任何东西都会被当作字符粗,所以最后编译器会自动为其添加0
,因此也就不用我们再手动添加0
了。
更进一步,我们也不用写数组的大小:
char num[] = "hello";
std::cout << num;
这种情况下,编译器会自动根据后面的字符串来计算我们这个字符数组需要多大的空间,更加方便。
由于字符串的特殊性与重要性,很多时候就有着比平常数组所没有的特性,比如你可以直接从控制台输入字符串:
char str[100];
std::cin>>str; //C++方式
scanf("%s",str); //c语言方式
如果你想要操作字符串,也同样有相应的函数可以做到这些:
strcmp("str1","str2"); //比较两个字符串是否相等
strcat("str1","str2"); //拼接两个字符串
这样的函数有很多,但无一例外使用起来都很麻烦,所以C++中直接提供了一个string
类可以让我们很方便的使用字符串。
#include<iostream>
#include<string>
using namespace std;
int main() {
string s="str1";
s += "str2";
string s1 = "str1str2";
cout<<(s == s1); //输出s和s1是否相等,结果为1则相等
}
可以看到,字符串string
类的使用比前面字符数组的使用要简单的多,其中更多实现细节可以参考文章:自制标准库。
四、结构体
上面数组存储数据,只能存储一堆类型相同的数组,但很多时候其实我们可能希望的是融合不同的数据类型放在一起。
比如一个学生的年龄可能为int类型,其名字可能为string类型,而这就是结构体的用武之地:
struct Stu
{
int age;
string name;
};
就像上面这样,首先第一步就是描述我们自己定义类型的数据属性。
struct
是C/C++中的关键字,用于表示后面的是一个结构体。Stu
是这个结构体的名字。{}
里面存放的就是这个结构体由哪些部分组成的,比如这里就是由一个int和一个string组成的。
这个步骤可以看作是我们在定义一个属于我们自己的新类型。
有了这个新类型之后,我们就能用它来声明变量了:
struct Stu
{
int age;
string name;
};
int main() {
Stu s1; //Stu类型的变量s1
s1.age = 20; //s1上的age赋值
s1.name = "小明"; //s1上的name赋值
}
这里的点.
你可以等价于中文字‘的’,上面就可以看作s1的age等于20,s1的name等于“小明”。
结构体的作用就在于此,它可以让我们自定义数据类型来组织数据,用起来非常的方便!
你完全可以将其当作普通数据类型一样使用,唯一的区别是在使用的时候要用点获取其内部的数据类型进行赋值、取值。
Stu s[100]; //100个Stu类型的变量
s[99].age=100;
s[0].name="小红";
关于结构体的更多内容可以参考文章:结构体。
五、联合体
与结构体相对的是联合体,虽然如今已经用的很少了,但还是有必要提一嘴。
它的目的是在内存空间较为稀缺的使用,让多个类型共用同一块内存:
#include<iostream>
using namespace std;
union Test
{
char c;
int value1;
int value2;
};
int main() {
Test t1;
std::cout << sizeof(t1) << std::endl;;
t1.value1 = 100;
std::cout << t1.value2;
}
就像上面的代码一样,它的使用和结构体是一样的,只是将关键字从struct
更换为了union
。
但不同之处在于,它的大小始终等于其内部最大变量的大小:
比如这段代码中,最大的就是int
为4字节,所以它的总大小就是4字节。
并且由于是共用同一块内存的原因,当我给value1
赋值后,可以发现输出的value2
的值和value1
值相等。
这种用法如今已经很少了,稍微理解一下即可,并不常用,这里不再过多介绍。
六、枚举
数组是同类型的集合,字符串是字符的集合,结构体与联合体是不同类型的集合,而枚举则是数字的集合。
它的作用在于给数字取个名字,其实际的应用可以参考文章:用枚举优化宏。
更多详解的介绍可以参考文章枚举与枚举类。
这里只简单介绍一下它的用法:
和前面的结构体、联合体写法一样,只不过是将关键字换成了enum
。
然后直接在里面写这样的格式即可:名字=数字
,各个语句之间用,
分隔。
其中后半部分=数字
可以省略,它编译器会自动赋值,这真的只是给数字取个名字而已。
七、指针
指针是C/C++语言中的精髓,同样也是一大难点,它同样属于复合类型,因为前面提到的数组,本质上就是指针:
int num[100];
int *num1=new int[100];
上面两种方式都是在分配100
份int
类型变量,就连使用起来也都可以一样:
num[0]=100;
num1[0]=999;
指针的含义就是,变量本身存储的是不是普通的值,而是另一块内存的地址:
比如这里的num
、num1
,实际上存储的就是这块内存的地址,用的十六进制表示。
更直接一点,可以通过取值符号来获取变量的内存地址存于指针中:
int a = 100;
int* p = &a;
这都是可以的,更详细的用法可以参考文章:指针与引用。
这里主要介绍一下两种数组的区别:
int num[100]; //内存会自动释放
int *num1=new int[100]; //内存需要手动释放
delete[] num1; //不用了之后,需要手动释放内存
上面注释中就是两者最主要的区别,并且与前者不同,使用new
来分配数组内存时,其数量是可以动态变化的:
int n;
cin>>n;
int num[n]; //错误
int *num=new int[n]; //正确
专业点来说就是,前者是在栈上分配的内存,后者是在堆上分配的,用的多了其实自然而然的就会了。
对于结构体而言,指针的使用方式和一般变量的使用方式还有点不一样:
#include<stdio.h>
struct Stu {
int a;
int b;
};
int main() {
Stu* s = new Stu;
(*s).a = 10;
(*s).b = 10;
s->a = 10;
s->b = 20;
}
想要访问结构体内部的字段值,从前面的介绍中已经知道,需要使用.
符号,但那仅限于得到结构体实例的前提下进行的。
而通过动态new关键字在堆上实例化的对象来说,我们拿到的只有它的指针,此时想要使用.
访问其上的字段,就需要先用*
获取其实例。
相比之下更简单的方式,其实是使用->
符号来访问字段属性,它是专门用于结构体、联合体、类这样的复合类型指针访问字段所设计的,使用起来很方便。
注意这个->
箭头符号,本质上其实是减号与大于号的组合。