友情链接:C/C++系列系统学习目录
知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上
结构,就是程序员自定义的一种“数据类型”,是使用多个基本数据类型、或者其它结构,组合而成的一种新的“数据类型”
有的时候,我们需要表示一些复杂信息时,使用单纯的数据结构类型很不方便,比如:学生信息(学号,姓名,班级,电话,年龄),为此,C提供了结构变量(structure variable)提高你表示数据的能力,它能让你创造新的形式。
在C primer plus一书中将此操作描述为声明
struct 结构名 {
成员类型 成员名;
成员类型 成员名;
....
};
//例如:
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
该声明并未创建实际的数据对象,只描述了该对象由什么组成。〔有时,我们把结构声明称为模板,因为它勾勒出结构是如何储存数据的。如果读者知道 C++的模板,此模板非彼模板,C++中的模板更为强大。〕
要以struct开头,它表明跟在其后的是一个结构
最后要用分号,表示结构布局定义结束。
各成员之间用分号隔开
结构中的成员允许也是一个结构类型
struct people {
struct book a;
struct book b;
struct book c;
};
拓展:
在C11中,可以用嵌套的匿名成员结构定义person:
struct person { int id; struct {char first[20]; char last[20];}; // 匿名结构 };
初始化的方式相同:
struct person ted = {8483, {"Ted", "Grass"}};
但是,在访问ted时简化了步骤,使用first和last即可:
puts(ted.first);
结构有两层含义。一层含义是“结构布局”,刚才已经讨论过了。结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。下一步是创建一个结构变量,即是结构的另一层含义。
struct book library;
创建了一个结构变量library
编译器使用book模板为该变量分配空间:一个内含MAXTITL个元素的char数组、一个内含 MAXAUTL个元素的char数组和一个float类型的变量。这些存储空间都与一 个名称library结合在一起
struct book所起的作用相当于一般声明中的int或 float。
可以定义两个struct book类型的变量,或者甚至是指向struct book类型结构的指针
声明结构的过程和定义结构变量的过程可以组合成一个步骤。 如下所示,组合后的结构声明和结构变量定义不需要使用结构标记,但是,如果打算多次使用结构模板,就要使用带标记的形式;或者,使用后面介绍的typedef。
struct { /* 无结构标记 */
char title[MAXTITL];
char author[MAXAUTL];
float value;
}library;
----------------------------------------------------------------
struct book library; //就计算机而言,该结构体变量的定义是以下声明的简化
struct book {
char title[MAXTITL];
char author[AXAUTL];
float value;
}library; /* 声明的右右花括号后跟变量名*/
typedef 可以为某一类型自定义名称,这样便可以使用BOOK代替struct book使用,达到即使没有结构标签名也能多次使用结构模板来定义变量的目的
typedef struct book { char title[MAXTITL]; char author[MAXAUTL]; float value; }BOOK; BOOK library1; BOOK library2;
用typedef来命名一个结构类型时,可以省略该结构的标签,但是当结构成员是本身的时候,必须要由结构体标识名来表示,这种情况就不能省略标签,不能用BOOK来表示
typedef struct{ char title[MAXTITL]; char author[MAXAUTL]; float value; }BOOK; //同样可以直接使用BOOK定义结构变量 BOOK library; ------------------------------------------------------------------ //当结构成员是本身的时候 typedef struct{ BOOK example; //这样是不行的 }BOOK; typedef struct book{ struct book example; //只能这样 }BOOK;
使用结构成员运算符——点(.)可以访问结构中的成员。例如:
library.value //访问library的value部分。可以像使用任何float类型变量那样使用library.value。
s_gets(library.title, MAXTITL);
scanf("%f", &library.value);
本质上,.title、.author和.value的作用相当于book结构的下标。
当结构成员也是结构时,使用两个.即可:
people.a.title
初始化变量和数组如下:
int count = 0; int fibo[7] = {0,1,1,2,3,5,8};
使用在一对花括号中括起来的初始化列表进行初始化, 各初始化项用逗号分隔。
struct book library = {
"The Pious Pirate and the Devious Damsel",
"Renee Vivotte",
1.95
};
结构中含有结构的初始化:
struct people {
{"The Pious Pirate and the Devious Damsel","Renee Vivotte",1.95},
{"THE ABC","QI",2.5},
{"THE CDE","CHEN",3.0}
};
拓展:指定初始化器
C99和C11为结构提供了指定初始化器,定义的时候我们可以指定初始化的属性,VS/VC不支持,但gcc支持
//可以按照任意顺序使用指定初始化器 struct book library = { .title = "The Pious Pirate and the Devious Damsel", .value = 1.95 };
声明(定义)结构数组:
struct book library[MAXBKS];
以上代码把library声明为一个内含MAXBKS个元素的数组。数组的每个元素都是一个book类型的数组。数组名library本身不是结构名,它是一个数组名,该数组中的每个元素都是struct book类型的结构变量。
为何要使用指向结构的指针:
用法:
声明(定义)和初始化结构指针
struct guy * him;
该声明并未创建一个新的结构,但是指针him现在可以指向任意现有的guy类型的结构。例如:
//如果barney是一个guy类型的结构,
him = &barney;
如果 fellows 是一个结构数组,数组名就是该数组的地址即首元素的地址,所以,要让 him 指向fellows[0],可以这样写:
him = fellows him = &fellow[0]; //him指向fellow[0],him+ 1指向fellow[1]
用指针访问成员
(1)使用 -> 符
如果him == &barney,那么him->income 即是 barney.income
如果him == &fellow[0],那么him->income 即是 fellow[0].income
(2)直接解引
如果him ==&fellow[0],那么*him == fellow[0],因此,可以做以下替代:
fellow[0].income == (*him).income
//必须要使用圆括号,因为.运算符比*运算符的优先级高。
(一)形参是结构变量成员
struct funds {
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(double x, double y)
{
return(x + y);
}
struct funds stan = {
"Garlic-Melon Bank",
4032.27,
"Lucky's Savings and Loan",
8543.94
};
sum(stan.bankfund, stan.savefund));
(二)形参使用结构体指针
如果需要在被调函数中修改主调函数中成员的值,就要传递成员的地址:
double sum(const struct funds * money) //由于该函数不能改变指针所指向值的内容,所以把money声明为一个指向const的指针。
{
return(money->bankfund + money->savefund);
}
sum(&stan.bankfund);//注意,必须使用&运算符来获取结构的地址。和数组名不同,结构体名不是结构体变量的地址
(三)形参直接使用结构变量
一个结构比一个 单独的值复杂,以前的C实现不允许把结构作为参数传递给函数。ANSI C允许把结构作为参数使用。所以程序员可以选择是传递结构本身,还是传递指向结构的指针。
double sum(struct funds moolah)/* 参数是一个结构 */
{
return(moolah.bankfund + moolah.savefund);
}
sum(stan);
(四)返回值使用结构体
现在的C(包括ANSI C),函数不仅能把结构本身作为参数传递,还能 把结构作为返回值返回。把结构作为函数参数可以把结构的信息传送给函数;把结构作为返回值的函数能把结构的信息从被调函数传回主调函数。结构指针也允许这种双向通信
struct programer{
char name[32];
int age;
int salary;
};
struct programer add_salary(struct programer p, int num){
p.salary += num;
return p;
}
struct programer xiaoniu;
strcpy(xiaoniu.name, "小牛");
xiaoniu.age = 28;
xiaoniu.salary = 20000;
xiaoniu = add_salary(xiaoniu, 5000);
现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做。也 就是说,如果n_data和o_data都是相同类型的结构,可以这样做:
o_data = n_data; // 把一个结构赋值给另一个结构
这条语句把n_data的每个成员的值都赋给o_data的相应成员。即使成员是数组,也能完成赋值。另外,还可以把一个结构初始化为相同类型的另一 个结构:
struct names right_field = {"Ruthie", "George"};
struct names captain = right_field; // 把一个结构初始化为另一个结构
把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这 种方法,而且执行起来很快,只需要传递一个地址。缺点是无法保护数据。 被调函数中的某些操作可能会意外影响原来结构中的数据。不过,ANSI C 新增的const限定符解决了这个问题。
把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。另外,代码风格也更清楚。传递结构的两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构传递给函数,而它只使用结构中的一两个成员时特别浪费。这种情况下传递指针或只传递函数所需的成员更合理。
struct names {
char first[LEN];
char last[LEN];
};
//可替代为:
struct pnames {
char * first;
char * last;
};
struct names veep = {"Talia", "Summers"};
struct pnames treas = {"Brad", "Fallingjaw"};
printf("%s and %s\n", veep.first, treas.first);
对于struct names类型的结构变量veep,以上字符串都储存在结构内部, 结构总共要分配40字节储存姓名
对于struct pnames类型的结构变量 treas,以上字符串储存在编译器储存常量的地方。结构本身只储存了两个地址,在我们的系统中共占16字节。尤其是,struct pnames结构不用为字符串分配任何存储空间。
注意:
struct names accountant; struct pnames attorney; puts("Enter the last name of your accountant:"); scanf("%s", accountant.last); puts("Enter the last name of your attorney:"); scanf("%s", attorney.last); /* 这里有一个潜在的危险 */
对于律师(attorney),scanf()把字符串放到attorney.last表示的地址上。由于这是未经初始化的变量,地址可以是任何 值,因此程序可以把名放在任何地方。如果走运的话,程序不会出问题,至 少暂时不会出问题,否则这一操作会导致程序崩溃。实际上,如果程序能正常运行并不是好事,因为这意味着一个未被觉察的危险潜伏在程序中。
因此如果要用结构储存字符串,用字符数组作为成员比较简单。用指向 char 的指针也行,但是误用会导致严重的问题。
对于注意中的问题,如果使用malloc()分配内存并使用指针储存该地址,那么在结构中使用指针处理字符串就比较合理:
struct namect {
char * fname; // 用指针代替数组
char * lname;
int letters;
};
void getinfo (struct namect * pst)
{
char temp[SLEN];
printf("Please enter your first name.\n");
s_gets(temp, SLEN);
// 分配内存储存名
pst->fname = (char *) malloc(strlen(temp) + 1);
// 把名拷贝到已分配的内存
strcpy(pst->fname, temp);
printf("Please enter your last name.\n");
s_gets(temp, SLEN);
pst->lname = (char *) malloc(strlen(temp) + 1);
strcpy(pst->lname, temp);
}
要理解这两个字符串都未储存在结构中,它们储存在 malloc()分配的内存块中。
由于结构可以储存不同类型的信息,所以它是构建数据库的重要工具。 例如,可以用一个结构储存雇员或汽车零件的相关信息。最终,我们要把这 些信息储存在文件中,并且能再次检索。数据库文件可以包含任意数量的此 类数据对象。储存在一个结构中的整套信息被称为记录(record),单独的 项被称为字段(field)。
#define MAXTITL40
#define MAXAUTL40
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
//如果pbook标识一个文件流,那么通过下面这条语句可以把储存在struct book类型的结构变量primer中的信息输出:
fprintf(pbooks, "%s %s %.2f\n", primer.title,primer.author, primer.value);
对于一些结构(如,有 30个成员的结构),这个方法用起来很不方便。另外,在检索时还存在问题,因为程序要知道一个字段结束和另一个字 段开始的位置。虽然用固定字段宽度的格式可以解决这个问题(例如,“%39s%39s%8.2f”),但是这个方法仍然很笨拙。
更好的方案是使用fread()和fwrite()函数读写结构大小的单元。例如:
fwrite(&primer, sizeof(struct book), 1, pbooks);
定位到 primer 结构变量开始的位置,并把结构中所有的字节都拷贝到 与 pbooks 相关的文件中。sizeof(struct book)告诉函数待拷贝的一块数据的大小,1 表明一次拷贝一块数据。带相同参数的fread()函数从文件中拷贝一块结构大小的数据到&primer指向的位置。简而言之,这两个函数一次读写整个记录,而不是一个字段。
联合(union)也是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。其典型的用法是,设计一种表以储存既无规律、事先也不知道顺序的混合类型。使用联合类型的数组,其中的联合都大小相等,每个联合可以储存各种数据类型。
union hold {
int digit;
double bigfl;
char letter;
};
union hold fit; // hold类型的联合变量
union hold save[10]; // 内含10个联合变量的数组
union hold * pu; // 指向hold类型联合变量的指针
有 3 种初始化的方法:把一个联合初始化为另一个同类型的联合;初始 化联合的第1个元素;或者根据C99标准,使用指定初始化器:
union hold valA;
valA.letter = 'R';
union hold valB = valA; // 用另一个联合来初始化
union hold valC = {88}; // 初始化联合的digit成员
union hold valD = {.bigfl = 118.2}; // 指定初始化器
fit.digit = 23; //把 23 储存在 fit,占2字节
fit.bigfl = 2.0; // 清除23,储存 2.0,占8字节
fit.letter = 'h'; // 清除2.0,储存h,占1字节
点运算符表示正在使用哪种数据类型。在联合中,一次只储存一个值。 即使有足够的空间,也不能同时储存一个char类型值和一个int类型值。和用指针访问结构使用->运算符一样,用指针访问联合时也要使用->运算符:
pu = &fit;
x = pu->digit; // 相当于 x = fit.digit
注意:
fit.letter = 'A'; flnum = 3.02*fit.bigfl; // 错误
以上语句序列是错误的,因为储存在 fit 中的是 char 类型,但是下一行 却假定 fit 中的内容是double类型。
枚举是 C/C++语言中的一种基本数据类型,它可以用于声明一组常数。当一个变量有几个固定的可能取值时,可以将这个变量定义为枚举类型。 比如,你可以用一个枚举类型的变量来表示季节,因为季节只有 4 种可能的取值:春天、夏 天、秋天、冬天。
(一)枚举类型的定义:
enum 枚举名 {枚举元素 1,枚举元素 2,……};
enum Season {spring, summer, autumn, winter};
可以用枚举类型(enumerated type)声明符号名称来表示整型常量(实际上,enum 常量是int类型,因此,只要能使用int类型的地方就可以使用枚举类型)
Season为标记名,把enum Season作为一个类型名使用,花括号内的标识符枚举了spectrum变量可能有的值。这些符号常量被称为枚举符,假设定义一个enum Season类型的变量s,然后便可以这样用:
s = spring;
if (s == yellow)
...;
枚举符是int类型的符号常量,但是枚举变量可以是任意整数类型,前提是该整数类型可以储存枚举常量。例如,Season的枚举符范围是 0~3,所以编译器可以用unsigned char来表示color变量。
默认情况下,枚举列表中的常量都被赋予为从0、1、2开始
拓展:名称空间(命名空间)
struct rect { double x; double y; }; int rect; // 在C中不会产生冲突
C语言使用名称空间(namespace)标识程序中的各部分,即通过名称来识别。作用域是名称空间概念的一部分:
- 两个不同作用域的同名变量不冲突;两个相同作用域的同名变量冲突。
- 名称空间是分类别的。在特定作用域中的结构标记、联合标记和枚举标记都共享相同的名称空间,该名称空间与 普通变量使用的空间不同。这意味着在相同作用域中变量和标记的名称可以相同,不会引起冲突,但是不能在相同作用域中声明两个同名标签或同名变量。
- 尽管如此,以两种不同的方式使用相同的标识符会造成混乱。另外, C++不允许这样做,因为它把标记名和变量名放在相同的名称空间中。
(二)枚举变量的定义:
前面只是定义了枚举类型,接下来就可以利用定义好的枚举类型定义变量,跟结构体一样, 有 3 种方式定义枚举变量
先定义枚举类型,再定义枚举变量
enum Season {
spring, //0
summer, //1
autumn, //2
winter //3
};
enum Season s;
可以为枚举常量指定整数值:
enum Season { spring, summer = 100, autumn, winter };
如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续递增的值。以上spring的值为0(默认),summer的值为100,autumn和winter的值为101和101
定义枚举类型的同时定义枚举变量
enum Season {
spring,
summer,
autumn,
winter
}s;
省略枚举名称,直接定义枚举变量
enum Season {
spring,
summer,
autumn,
winter
}s;
(一)赋值:可以给枚举变量赋枚举常量或者整型值
#include
int main() {
// 1.定义枚举类型
enum Season {spring, summer, autumn, winter} s;
// 2.定义枚举变量
s = spring; // 等价于 s = 0;
printf("%d\n", s);
s = winter;//等价于 s = 3;
printf("%d\n", s);
return 0;
}
//打印结果 :
0
3
(二)遍历枚举元素
enum Season {spring, summer, autumn, winter} s;
// 遍历枚举元素
for (s = spring; s <= winter; s++) {
printf("枚举元素:%d \n", s);
}
输出结果: 枚举元素:0
枚举元素:1
枚举元素:2
枚举元素:3
typedef工具是一个高级数据特性,利用typedef可以为某一类型自定义名称。这方面与#define类似,但是两者有3处不同:
用法:
假设要用BYTE表示1字节的数组。只需像定义个char类型变量一样定义BYTE,然后在定义前面加上关键字typedef即可:
typedef unsigned char BYTE;
//随后,便可使用BYTE来定义变量:
BYTE x, y[10], * z;
作用:
简化写法,表明意图:
比如用BYTE代替unsigned char表明你打算用BYTE类型的变量表示数字,而不是字符码。
可以把typedef用于结构:
typedef struct complex {
float real;
float imag;
} COMPLEX;
然后便可使用COMPLEX类型代替complex结构来表示复数。用typedef来命名一个结构类型时,可以省略该结构的标签:
typedef struct {
double x;
double y;
} rect;
typedef常用于给复杂的类型命名。例如, 下面的声明:
//定义复杂类型的函数
//把FRPTC声明为一个函数类型,该函数返回一个指针,该指针指向内含 5个char类型元素的数组
typedef char (* FRPTC ()) [5];
char (* fun())[5]{ ... }
FRPTC *pf; // pf即为指向该函数类型的函数指针
pf = fun;
//定义指向特定类型函数的指针的别名:
//新建一个类型PFI,是指向有两个参数、参数类型为char*、返回值为int的函数的指针
typedef int(*PFI)(char *,char *);
int strcmp(char *s,char *t);
PFI = strcmp;
提高程序的可移植性:
C标准规定sizeof和time()返回整数类型,但是让实 现来决定具体是什么整数类型。其原因是,C 标准委员会认为没有哪个类型 对于所有的计算机平台都是最优选择。所以,标准委员会决定建立一个新的类型名(如,time_t),并让实现使用typedef来设置它的具体类型。我们移植的时候,只需要修改它的typedef部分即可
typedef与#define的主要目的都是为了让代码更加简洁,都有文本替换的功能,二者的不同之处在于,typedef由于可以被编译器理解,所以文本替换也比宏要强大的多,宏只能做简单的文本替换,例如:
typedef char * STRING;
STRING name, sign;
//相当于:
char * name, * sign;
//如果是:
#define STRING char *
STRING name, sign;
//将被翻译成:
char * name, sign;
//这导致只有name才是指针。