1.指针入门到精通
- 指针是什么
如果在程序中定义了一个变量,在对程序进行编译时,系统就会给该变量分配内存单元,编译系统根据程序中定义的变量类型,分配一定长度的空间。
内存区的每一个字节有一个编号,这就是“地址”,它相当于旅馆中的房间号。在地址所标识的内存单元中存放数据,相当于旅馆房间中的旅客一样。由于通过地址能找到所需的变量单元,我们可以说,地址指向该变量单元,将地址形象化地称为“指针”。
#include <stdio.h>
int main()
{
int i = 5, j = 9, k;
printf("i=%p, j=%p, k=%p\n", &i, &j, &k);
int *i_pointer = &i; // 将i变量的地址赋给指针变量i_pointer
// &指针变量,即是指针变量本身的地址
// *指针变量,即是指针变量所指向的值
// 指针变量,即是指针变量所存放的地址
printf("i_pointer=%p, i_pointer=%d, i_pointer=%p\n", &i_pointer, *i_pointer, i_pointer);
return 0;
}
#include <stdio.h>
int main()
{
int i = 5;
int *i_pointer = &i;
printf("i=%d\n", *i_pointer);
*i_pointer = 200;
printf("i=%d\n", i);
return 0;
}
直接存取与间接存取
为了表示将数值3送到变量中,可以有两种表达方法:
直接存取:将3直接送到变量i所标识的单元中,例如:
i=3
间接存取:将3送到变量i_pointer所指向的单元(即变量i的存储单元),例如:
*i_pointer=3
,其中*i_pointer
表示i_pointer指向的对象。一个变量的地址称为该变量的指针,如果有一个变量专门用来存放另一变量的地址(即指针),则称它为“指针变量”,指针变量就是地址变量,用来存放地址的变量,指针变量的值是地址(即指针)。
指针和指针变量是不同的概念。
- 指针变量
定义指针变量的一般形式为:
类型 *指针变量名;
int是为指针变量指定的“基类型”,基类型指定指针变量可指向的变量类型。例如:float *pointer_3;
在引用指针变量时,可能有三种情况:
给指针变量赋值,如:
*p = &a;
引用指针变量指向的变量,如:
*p = &a; *p = 1;
引用指针变量的值,如:
printf("%o", p); // 以八进制输出a的地址
要熟练掌握两个有关的运算符:
&
取地址符,&a
是变量a的地址;
*
指针运算符(间接访问运算符),如果p指向变量a,则*p
就代表a。
#include <stdio.h>
void swap(int *px, int *py)
{
int temp;
temp = *px;
*px = *py;
*py = temp;
}
int main()
{
int a, b;
int *pa, *pb;
printf("请输入两个整数a,b:\n");
scanf_s("%d,%d", &a, &b);
pa = &a;
pb = &b;
if (a < b)
swap(pa, pb);
printf("max=%d,min=%d", a, b);
return 0;
}
- 指针引用数组
数组元素的指针:
一个变量有地址,一个数组包含若干元素,每个数组元素都有相应的地址。指针变量可以指向数组元素(把某一元素的地址放到一个指针变量中)。所谓数组元素的指针就是数组元素的地址。
*p = &a[0];
等价于*p = a
,数组名a不代表整个数组,只代表数组首元素的地址。p = a
的作用是把a数组的首元素的地址赋给指针变量p,而不是把数组a各元素的值赋给p。
#include <stdio.h>
int main()
{
int a[10] = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
int* pa = a;
for (int i = 0; i < 10; i++)
printf("%4d", *(pa+i));
printf("\n");
return 0;
}
在引用数组元素时指针的运算:
+ += - -= p++ ++p p-- --p
两个指针相减,比如
p1-p2
,只有p1和p2都指向同一数组中的元素时才有意义。通过指针引用数组元素:
引用一个数组元素,可用下面两种方法:下标法,如
a[i]
形式;指针法,如*(a+i)
或*(p+i)
,其中a是数组名,p是指向数组元素的指针变量,其初值p=a
用数组名作函数参数:
用数组名作函数参数时,因为实参数组名代表该数组元素的首地址,形参应该是一个指针变量,C编译都是将形参数组名作为指针变量来处理的。
通过指针引用多维数组:
行指针与列指针。
- 指针引用字符串
字符串是存放在字符数组中的,引用一个字符串,可以用以下两种方法:
用字符数组存放一个字符串,可以通过数组名和格式声明"%s"输出该字符串,也可以通过数组名和下标引用字符串中一个字符;
用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
int main()
{
char str[50] = "I Love C/C++!";
printf("str=%s\n", str);
char* pstr = "I Love C/C++!";
printf("str=%s\n", pstr);
return 0;
}
- 指针函数与函数指针
指针函数:
是指带指针的函数,即本质是一个函数,函数返回类型是某一类型的指针。
类型标识符 *函数名(参数列表)
,int *f(x, y)
首先它是一个函数,只不过这个函数的返回值是一个地址。函数返回值必须用同类型的指针变量来接收,也就是说,指针函数一定有函数返回值,而且在主调函数中,函数返回值必须赋给同类型的指针变量。
#include <stdio.h>
int* getDateFunc(int week, int day)
{
static int cale[5][7] =
{
{1, 2, 3, 4, 5, 6, 7},
{8, 9, 10, 11, 12, 13, 14},
{15, 16, 17, 18, 19, 20, 21},
{22, 23, 24, 25, 26, 27, 28},
{29, 30, 31, -1}
};
return &cale[week - 1][day - 1];
}
int main()
{
int wk, dy;
do
{
printf("请输入week(1-5),day(1-7)");
scanf_s("%d,%d", &wk, &dy);
} while (wk < 1 || wk>5 || dy < 1 || dy>7);
printf("%d\n", *getDateFunc(wk, dy));
return 0;
}
函数指针:
函数指针是指向函数的指针变量,即本质是一个指针变量。
int (*f) (int x); // 声明一个函数指针
,f = func; // 将func函数的首地址赋给指针f
指向函数的指针包含了函数的地址的入口地址,可以通过它来调用函数。声明格式如下:
类型说明符 (*函数名) (参数)
其实这里不能称为函数名,应该叫做指针的变量名。这个特殊的指针指向一个返回整型值的函数,指针的声明必须和它指向函数的声明保持一致。
指针名和指针运算符外面的括号改变了默认的运算符优先级。如果没有圆括号,就变成了一个返回整型指针的函数的原型声明。
#include <stdio.h>
void (*Pfunc)();
void a()
{
printf("调用函数a()\n");
}
void b()
{
printf("调用函数b()\n");
}
int main()
{
Pfunc = a;
(*Pfunc)();
Pfunc = b;
(*Pfunc)();
return 0;
}
- 指针数组与数组指针
指针数组:
可以说成是“指针的数组”,首先这个变量是一个数组,其次,“指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。
#include <stdio.h>
int main()
{
char* PointerArr[5] = { "你", "是", "学", "生", "!" };
for (int i = 0; i < 5; i++)
printf("%s", PointerArr[i]);
printf("\n");
return 0;
}
数组指针:
数组指针可以说成是“数组的指针”,首先这个变量是一个指针,其次,“数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。
#include <stdio.h>
int main()
{
char Arr[][4] = {"妹", "妹", "你", "坐", "船", "头", "!"};
// 在二维数组的声明中,结合行序优先的规律看,其实先声明了一个Arr[]数组,
// 含有N个元素,每个元素是char [4]类型,如果被声明的类型是char *,则与
// char [4]类型不符,所以必须是char(*pi)[4] = Arr;
char(*pi)[4] = Arr; // []表示定义一个数组,4表示数组中有4个元素,int表示数组为整型数组,*表示pi是一个指针变量
for (int i = 0; i < 7; i++)
printf("%s", *(pi+i));
printf("\n");
return 0;
}
- 动态内存分配详解
非静态的局部变量时分配在内存中的动态存储区的,这个存储区是一个称为栈的区域,C语言还允许建立内存动态分配区域,以存放一些临时用的数据,这些数据需要时随时开辟,不需要时随时释放。这些数据是临时存放在一个特别的自由存储区,称为堆区。
对内存的动态分配是通过系统提供的库函数来实现的,主要有
malloc,calloc,free,realloc
四个函数。malloc函数:
void *malloc(unsigned int size);
其作用是在内存的动态存储区中分配一个长度为size的连续空间,函数的值是所分配区域的第一个字节的地址,或者说,此函数是一个指针型函数,返回的指针指向该分配区域的开头位置。
malloc(100)
开辟100字节的临时分配域,函数值为其第一个字节的地址。注意指针的基类型为void,即不指向任何类型的数据,只提供一个地址,如果此函数未能成功地执行(例如内存空间不足),则返回空指针(NULL)。
calloc函数:
void *calloc(unsigned n, unsigned size);
其作用是在内存的动态存储区中分配n个长度为size的连续空间,这个空间一般比较大,足以保存一个数组。
用calloc函数可以为一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。这就是动态数组,函数返回值指向所分配域的起始位置的指针;如果分配不成功,返回NULL。
例如
p = calloc(4, 50);
,开辟50*4个字节的临时分配域,把起始地址赋给指针变量p。free函数:
void free(void *p);
其作用是释放指针变量p所指向的动态空间,使这部分空间能重新被其他变量使用。p应是最近一次调用calloc或malloc函数时得到的函数返回值。
free(p);
释放指针变量p所指向的已分配的动态空间,free函数无返回值。realloc函数:
void *realloc(void *p, unsigned int size);
如果已经通过malloc函数calloc函数获得了动态空间,想改变其大小,可以用realloc函数重新分配。
用realloc函数将p所指向的动态空间大小改变为size,p的值不变。如果重新分配不成功,返回NULL。
realloc(p, 50);
将p所指向的已分配的动态空间改为50字节。以上四个函数的声明在stdlib.h头文件中。
#include <stdio.h>
#include <stdlib.h>
void checkScore(int *ps)
{
printf("不及格成绩如下:\n");
for (int i = 0; i < 5; i++)
if (ps[i] < 60)
printf("%5d", ps[i]);
printf("\n");
}
int main()
{
int* ps;
ps = (int*)malloc(5 * sizeof(int));
if (ps)
{
printf("请输入5名学生的成绩:\n");
for (int i = 0; i < 5; i++)
scanf_s("%d", ps + i);
printf("输入的学生成绩为:\n");
for (int i = 0; i < 5; i++)
printf("%5d", *(ps + i));
printf("\n");
checkScore(ps);
}
return 0;
}
2.结构体类型
- 定义和使用结构体变量
用户自己建立由不同类型数据组成的组合型的数据结构,它称为结构体。声明一个结构体类型的一般形式为:
struct 结构体名 {成员列表};
结构体类型并非只有一种,而是可以设计出许多种结构体类型;
成员可以属于另一个结构体类型。
struct Student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
前面只是建立了一个结构体类型,它相当于一个模型,并没有定义变量,其中并无具体数据,系统对之也不分配存储单元。相当于设计好了图纸,但并未建成具体的房屋,为了能在程序中使用结构体类型的数据,应当定义结构体类型的变量,并在其中存放具体的数据。
定义结构体类型变量:
1)先声明结构体类型,再定义该类型变量:声明结构体类型struct Student,可以用它来定义变量:
struct Student student1, student2;
2)在声明类型的同时定义变量。
struct Student
{
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
}student1, student2;
3)不指定类型名而直接定义结构体类型变量。
其一般形式为:
struct {成员列表}变量名列表;
,指定了一个无名的结构体类型。结构体变量的初始化
#include <stdio.h>
struct date
{
int year;
int month;
int day;
};
struct student
{
int inum;
char cname[10];
struct date birthday; // 使用结构体类型定义结构体变量(结构体变量作为结构体的成员)
double score;
}st3 = { 103, "王五", {2020, 2, 29}, 92.0 },st4 = { 104, "赵六", {2020, 5, 29}, 58.0 };
struct student st1 = { 101, "张三", {2020, 12, 29}, 98.0 }, st2 = { 102, "李四", {2018, 12, 29}, 94.0 };
int main()
{
printf("No.1:%d %s %d-%d-%d %.1lf\n", st1.inum, st1.cname, st1.birthday.year, st1.birthday.month,st1.birthday.day, st1.score);
printf("No.2:%d %s %d-%d-%d %.1lf\n", st2.inum, st2.cname, st2.birthday.year, st2.birthday.month, st2.birthday.day, st2.score);
printf("No.3:%d %s %d-%d-%d %.1lf\n", st3.inum, st3.cname, st3.birthday.year, st3.birthday.month, st3.birthday.day, st3.score);
printf("No.4:%d %s %d-%d-%d %.1lf\n", st4.inum, st4.cname, st4.birthday.year, st4.birthday.month, st4.birthday.day, st4.score);
return 0;
}
- 使用结构体数组
定义结构体数组:
定义结构体数组的一般形式是:
1)struct 结构体名
{ 成员列表 } 数组名[数组长度]
2)先声明一个结构体类型,然后再用此类型定义结构体数组:
结构体类型 数组名[数组长度]
struct Person leader[3];
对结构体数组初始化的形式是:在定义数组的后面加上 = {初值列表};
struct Person leader[3] = {"li",0,"zhang",0,"fu",0};
#include <stdio.h>
#include <string.h>
struct PersonS
{
char name[20];
int iCount;
}leader[3] = {
{"li", 0}, {"wang", 0}, {"zhang", 0}
};
int main()
{
char lname[20]; // 投票人输入姓名存放在lname
lname[19] = '\0';
for (int i = 0; i < 10; i++)
{
printf("请输入候选人的姓名(li zhang wang)");
scanf_s("%s", lname, 19);
for (int j = 0; j < 3; j++)
{
if (strcmp(lname, leader[j].name) == 0)
leader[j].iCount++;
}
}
for (int i = 0; i < 3; i++)
printf("候选人%s的票数为:%d\n", leader[i].name, leader[i].iCount);
return 0;
}
- 结构体指针
指向结构体变量的指针:
指向结构体对象的指针变量既可以指向结构体变量,也可以用来指向结构体数组中的元素。指针变量的基类型必须与结构体变量的类型相同。如:
struct Student *pt
为了方便和直观,C语言允许把(*p).num用p->num来代替。
指向结构体数组的指针:
#include <stdio.h>
struct date
{
int year;
int month;
int day;
};
struct student
{
int inum;
char cname[20];
struct date birthday;
double score;
}st[4] = {
{ 101, "张三", {2020, 2, 29}, 92.0 },
{ 102, "李四", {2019, 2, 29}, 93.0 },
{ 103, "王五", {2018, 2, 29}, 94.0 },
{ 104, "赵六", {2017, 2, 29}, 95.0 }
};
int main()
{
struct student* ps = st;
printf("【结构体数组输出学生信息】\n");
for (int i=0; i<4; i++)
printf("No.%d:%d %s %d-%d-%d %.1lf\n", i+1, st[i].inum, st[i].cname, st[i].birthday.year, st[i].birthday.month, st[i].birthday.day, st[i].score);
printf("【结构体指针第一种形式输出学生信息】\n");
for (int i=0; i<4; i++)
printf("No.%d:%d %s %d-%d-%d %.1lf\n", i+1, (*(ps+i)).inum, (*(ps + i)).cname, (*(ps + i)).birthday.year, (*(ps + i)).birthday.month, (*(ps + i)).birthday.day, (*(ps + i)).score);
printf("【结构体指针第二种形式输出学生信息】\n");
for (int i=0; i<4; i++)
printf("No.%d:%d %s %d-%d-%d %.1lf\n", i + 1, (ps + i)->inum, (ps + i)->cname, (ps + i)->birthday.year, (ps + i)->birthday.month, (ps + i)->birthday.day, (ps + i)->score);
return 0;
}
用结构体变量和结构体变量的指针作为函数参数:
将一个结构体变量的值传递给另一个函数,有三个方法:
1)用结构体变量的成员作参数
2)用结构体变量作实参
用结构体变量作实参时,将结构体变量所占的内存单元的内容全部按顺序传递给形参,形参也必须是同类型的结构体变量。在函数调用期间形参也要占用内存单元。这种传递方式在空间和时间上开销较大,在被调用函数期间改变形参(也是结构体变量)的值,不能返回主调函数。一般较少用这种方法。
3)用指向结构体变量(或数组元素)的指针作实参,将结构体变量(或数组元素)的地址传给形参。
3.常用字符串函数
- puts函数
puts()函数用来向标准输出设备(屏幕)输出字符串并换行,具体为:把字符串输出到标准输出设备,将'\0'转换为回车换行。其调用方式为,
puts(s);
,其中s为字符串字符(字符串数组名或字符串指针)功能:将字符串输出到终端,puts函数一次只能输出一个字符串,字符串中可以包括转义字符。
用法:
int puts(const char *string);
如果成功,返回非负值。错误则函数返回EOF。
#include <stdio.h>
#include <string.h>
int main()
{
// char str1[100];
// 避免未初始化的警告
// strcpy_s(str1, 100, "Hello World.\n");
char *str1 = "Hello World.\n";
puts("Hello C/C++!");
printf("%d", puts(str1));
return 0;
}
- gets函数
gets从标准输入设备读字符串函数,其可以无限读取,不会判断上限,以回车结束读取,所以程序员应该确保buffer的空间足够大,以便在执行读操作时不发生溢出。
功能:从stdin流中读取字符串,直至接收到换行符或EOF时停止,并将读取的结果存放在buffer指针所指向的字符串数组中。换行符不作为读取串的内容,读取的换行符被转换为'\0'空字符,并由此来结束字符串。
char *gets(char *str);
#include <stdio.h>
int main()
{
char str[200];
printf("请输入一个字符串:\n");
gets_s(str, sizeof(str));
printf("输入的字符串为:\n");
puts(str);
return 0;
}
- strcat函数
extern char *strcat(char *dest, const char *src);
头文件:
在C中,函数原型存在于
<string.h>
头文件中;在C++中,函数原型存在于
<cstring.h>
头文件中。功能:把src所指向的字符串(包括'\0')复制到
*dest
所指向的字符串的后面(删除*dest
原来末尾的'\0')。要保证*dest
足够长,以容纳被复制进来的*src
,*src
中原有的字符串不变。返回指向dest的指针。
#include <stdio.h>
#include <string.h>
int main()
{
char str1[200] = "Hello ", str2[50] = "world";
strcat_s(str1, sizeof(str1), str2);
printf("拼接后的结果为:%s\n", str1);
return 0;
}
- strcpy函数
strcpy是一种C语言的标准库函数,strcpy把含有'\0'结束符的字符串复制到另一个地址空间,返回值的类型为char*。
char *strcpy(char *dest, const char *src);
功能:把从src地址开始且含有NULL结束符的字符串复制到以dest开始的地址空间。
#include <stdio.h>
#include <string.h>
int main()
{
char str1[200] = { 0 }, str2[50] = "Hello World";
strcat_s(str1, strlen(str1)+strlen(str2)+1, str2);
printf("复制后的结果为:%s\n", str1);
return 0;
}
- strncpy函数
char *strncpy(char *dest, char *source, int n);
strncpy函数用于将指定长度的字符串复制到字符数组中,表示把src所指向的字符串中以src地址开始的前n个字节复制到dest所指的数组中,并返回被复制后的dest。
#include <stdio.h>
#include <string.h>
int main()
{
char str1[100], str2[50] = "Hello Hahaha";
strncpy_s(str1, sizeof(str1), str2, 6);
printf("复制后的结果为:%s\n", str1);
return 0;
}
- strcmp函数
strcmp函数时string compare的缩写,用于比较两个字符串并根据比较结果返回整数。
extern int strcmp(const char *s1, const char *s2);
当s1 < s2时,返回-1;
当s1 = s2时,返回0;
当s1 > s2时,返回1;
#include <stdio.h>
#include <string.h>
int main()
{
char str1[50] = "Hello", str2[50] = "Hello Hahaha";
printf("比较结果为:%d\n", strcmp(str1, str2));
return 0;
}
- strlen函数
strlen所做的是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符'\0'为止,然后返回计数器值(长度不包含'\0')。
extern unsigned int strlen(char *s);
#include <stdio.h>
#include <string.h>
int main()
{
char str[100] = "hello world!";
printf("长度为:%d\n", strlen(str));
return 0;
}
- strlwr函数
strlwr函数的功能是将字符串转换为小写形式。
extern char *strlwr(char *s);
只转换s参数中出现的大写字母,不改变其它字符。返回指向s参数的指针。
- strupr函数
strupr函数的功能是将字符串转换为大写形式。
#include <stdio.h>
#include <string.h>
int main()
{
char str[100] = "hello world!HAHAHA";
_strlwr_s(str, sizeof(str));
printf("转换小写%s\n", str);
_strupr_s(str, sizeof(str));
printf("转换大写%s\n", str);
return 0;
}
4.枚举、共用体、位运算
- 枚举类型
如果一个变量只有几种可能的值,则可以定义为枚举类型。
所谓枚举,就是值把可能的值一一列举出来,变量的值只限于列举出来的值的范围内。
声明枚举类型用enum开头。
enum Weekday{sun, mon, tue, wed, thu, fri, sat};
声明了一个枚举类型enum Weekday;然后可以用此类型来定义变量。
enum Weekday workday,weekend;
C编译对枚举类型的枚举元素按常量处理,故称枚举常量。不要因为它们是标识符(有名字)而把它们看作变量,不能对它们赋值。
每一个枚举元素都代表一个整数,C语言编译按定义时的顺序默认它们的值为0,1,2,3,4,5......
如果有赋值语句:
workday=mon;
相当于workday=1;
也可人为地定义枚举元素的数值,如:
enum Weekday{sun=7,mon=1,tue,wed,thu,fri,sat};
,指定枚举常量sun的值为7,mon为1,以后顺序加1,sat为6.枚举元素可以用来作判断比较,枚举元素的比较规则是按其在初始化时指定的整数来进行比较的。
#include <stdio.h>
int main()
{
enum Weekday {Mon=1, Tue, Wed, Thur, Fri, Sat, Sun} day;
printf("请输入1-7:\n");
scanf_s("%d", &day);
switch (day)
{
case Mon:
printf("Monday\n");
break;
case Tue:
printf("Tuesday\n");
break;
case Wed:
printf("Wednesday\n");
break;
case Thur:
printf("Thursday\n");
break;
case Fri:
printf("Friday\n");
break;
case Sat:
printf("Saturday\n");
break;
case Sun:
printf("Sunday\n");
break;
default:
printf("输入数字有误\n");
}
return 0;
}
- 共用体类型
有时想用同一段内存单元存放不同类型的变量。使几个不同的变量共享同一段内存的结构,称为“共用体”类型的结构。
定义共用体类型变量的一般形式为:
union 共用体名
{成员列表} 变量列表;
共用体与结构体的定义形式相似,但它们的含义是不同的。结构体变量所占内存长度是各成员占的内存长度之和,每个成员分别占有自己的内存单元。而共用体变量所占的内存长度等于最长的成员的长度。
引用共用体变量的方式:只有先定义了共用体变量才能引用它,但应注意,不能引用共用体变量,而只能引用共用体变量中的成员。
共用体类型数据的特点:
1)同一个内存段可以用来存放几种不同类型的成员,但在每一瞬时只能存放其中一个成员,而不是同时存放几个;
2)可以对共用体变量初始化,但初始化表中只能有一个常量;
3)共用体变量中起作用的成员是最后一次被赋值的成员,在对共用体变量中的一个成员赋值后,原有变量存储单元中的值就被取代;
4)共用体变量的地址和它的各成员的地址都是同一地址;
5)不能对共用体变量名赋值,也不能企图引用变量名来得到一个值;
6)以前的C规定不能把共用体变量作为函数参数,但可以使用指向共用体变量的指针作函数参数。C99允许共用体变量作为函数参数;
7)共用体类型可以出现在结构体类型定义中,也可以定义共用体数组。反之,结构体也可以出现在共用体类型定义中,数组也可以作为共用体的成员。
#include <stdio.h>
struct sdata
{
int i;
char c;
double d;
};
union udata
{
int i;
char c;
double d;
};
int main()
{
struct sdata s; // 结构体变量
union udata u; // 共用体变量
printf("s=%d\n", sizeof(s)); // 16
printf("u=%d\n", sizeof(u)); // 8
return 0;
}
#include <stdio.h>
union udata
{
int i;
char c;
double d;
};
int main()
{
union udata u;
u.i = 10;
printf("%d,%c,%.2lf\n", u.i, u.c, u.d);
u.c = 'A';
printf("%d,%c,%.2lf\n", u.i, u.c, u.d);
u.d = 67.33;
printf("%d,%c,%.2lf\n", u.i, u.c, u.d);
return 0;
}
- 位运算
C 语言程序可以用独立的位或多个组合在一起的位来存储信息。文件访问许可就是一个常见的应用案例。位运算允许对一个字节或更大的数据单位中独立的位做处理:可以清除、设定(对于每个位置的结果,1=设定,0=清除),或者倒置任何位或多个位。也可以将一个整数的位模式向右或向左移动。
运算符:
& | ^ ~
移位运算:
移位运算符将左操作数的位模式移动数个位置,至于移动几个位置,由右操作数指定。
>> <<
#include <stdio.h>
int main()
{
int x = 5, y = 9, z = 0;
printf("x=%d,y=%d,z=%d\n", x, y, z);
z = x & y;
printf("x=%d,y=%d,z=%d\n", x, y, z);
int k = 10;
printf("k<<1=%d\n", k << 1);
printf("k>>1=%d\n", k >> 1);
return 0;
}
5.typedef与预处理
- typedef
简单地用一个新的类型名代替原有的类型名:
typedef int Integer;
#include <stdio.h>
typedef int INTLOTUSLAW;
int main()
{
int x = 90;
INTLOTUSLAW y = 200;
printf("x=%d\n", x);
printf("y=%d\n", y);
printf("x=%d\n", sizeof(x));
printf("y=%d\n", sizeof(y));
return 0;
}
命名一个简单的类型名代替复杂的类型表示方法:
1)命名一个新的类型名代表结构体类型
typedef struct
{int month; int day; int year;} Date;
Date birthday;
Date *p;
2)命名一个新的类型名代表数组类型
typedef int Num[100];
Num a;
3)命名一个新的类型名代表一个指针类型
typedef char *String;
String p, s[10];
4)命名一个新的类型名代表指向函数的指针类型
typedef int (*Pointer)();
Pointer p1, p2;
说明:
1)以上方法实际上是为特定的类型指定了一个同义词
2)用typedef只是对已经存在的类型指定一个新的类型名,而没有创造新的类型
3)用typedef声明数组类型、指针类型、结构体类型、共用体类型、枚举类型等,使得编程更加方便
4)typedef与#define表面上有相似之处
5)当不同源文件中用到同一类型数据时,常用typedef声明一些数据类型。可以把所有的typedef名称声明单独放在一个头文件中,然后在需要用到它们的文件中用#include指令把它们包含到文件中。这样编程者就不需要在各文件中自己定义typedef名称了。
6)使用typedef名称有利于程序的通用与移植。有时程序会依赖于硬件特性,用typedef类型就便于移植。
- 预处理
预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所做的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中的任何位置。
预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
宏定义:
C语言源程序中允许使用一个标识符来表示一个字符串,称为“宏”。被定义为宏的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为宏替换或宏展开。
无参宏定义:
无参宏的宏名后不带参数,
#define 标识符 字符串
,其中“#”表示这是一条预处理命令,“define”为宏定义命令,“标识符”为符号常量,即宏名。“字符串”可以是常数、表达式、格式串等。注意没有分号。带参宏定义:
C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。对带参数的宏,调用中,不仅要宏展开,而且要用实参去代换形参。
带参宏定义的一般形式为:
#define 宏名(形参表) 字符串
,带参宏调用的一般形式为:宏名(实参表)
#include <stdio.h>
#define MAX 10000
#define INC(x) x+1
int main()
{
printf("max=%d\n", MAX);
printf("x=%d\n", INC(19));
return 0;
}
文件包含:
#include
叫做文件包含命令,用来引入对应的头文件(.h文件)。#include
也是C语言预处理命令的一种。
#include
的用法有两种,如下所示:
#include <stdHeader.h>
#include "myHeader.h"
使用
<>
和""
的区别在于头文件的搜索路径不同:使用尖括号,编译器会到系统路径下查找头文件;
使用双引号,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。
条件编译:
条件编译是指预处理器根据条件编译指令,有条件地选择源程序代码中的一部分代码作为输出,送给编译器进行编译。主要是为了有选择性地执行相应操作,防止宏替换内容(如文件等)的重复包含。
条件编译指令 说明 #if 如果条件为真,则执行相应操作 #elif 如果前面条件为假,而该条件为真,则执行相应操作 #else 如果前面条件均为假,则执行相应操作 #endif 结束相应的条件编译指令 #ifdef 如果该宏已定义,则执行相应操作 #ifndef 如果该宏没有定义,则执行相应操作
#if-#else-#endif
:其调用格式为:
#if 条件表达式
程序段1
#else
程序段2
#endif
#include <stdio.h>
#define N 200
int main()
{
#ifdef N
printf("N\n");
#else
printf("M\n");
#endif
return 0;
}
6.文件操作知识
- 文件基础知识
什么是文件:
文件有不同的类型,在程序设计中,主要用到两种文件:
1)程序文件:包括源程序文件(后缀.c)、目标文件(后缀.obj)、可执行文件(后缀.exe)等。这种文件的内容是程序代码。
2)数据文件:文件的内容不是程序,而是供程序运行时读写的数据,如在程序运行过程中输出到磁盘(或其他外部设备)的数据,或在程序运行过程中供读入的数据。
操作系统把各种设备都统一作为文件处理,从操作系统的角度看,每一个与主机相连的输入输出设备都看作是文件,例如:终端键盘是输入文件,显示屏和打印机是输出文件。
文件指存储在外部介质上数据的集合。一批数据是以文件的形式存放在外部介质上的,操作系统是以文件为单位对数据进行管理,想找存放在外部介质上的数据,先按文件名找到所指定的文件,然后再从该文件读取数据,要向外部介质上存储数据,也必须先简历一个文件(以文件名作为标志),才能向它输出数据。
输入输出是数据传送的过程,数据如流水一样从一处流向另一处,因此常将输入输出形象地称为流(stream),即数据流。流表示了信息从源到目的端的流动。
输入操作时,数据从文件流向计算机内存,输出操作时,数据从计算机流向文件,无论是用Word打开或保存文件,还是C程序中的输入输出都是通过操作系统进行的,“流”是一个传输通道,数据可以从运行环境流入程序中,或从程序流至运行环境。
从C程序的观点来看,无论程序一次读写一个字符,或一行文件,或一个指定的数据区,作为输入输出的各种文件或设备都是统一以逻辑数据流的方式出现的。C语言把文件看作是一个字符(或字节)的序列。一个输入输出流就是一个字符流或字节(内容为二进制数据)流。
C语言的数据文件由一连串的字符(或字节)组成,而不考虑行的界限,两行数据间不会自动加分隔符,对文件的存取是以字符(字节)为单位的。输入输出数据流的开始和结束仅受程序控制而不受物理符号(如回车换行符)控制,这就增加了处理的灵活性。这种文件称为流式文件。
文件名:
文件要有一个唯一的文件标识,以便用户识别和引用。文件标识包括三部分:文件路径、文件名主干、文件后缀。
文件的分类:
根据数据的组织形式,数据文件可分为ASCII文件和二进制文件。数据在内存中是以二进制形式存储的,如果不加转换地输出到外存,就是二进制文件。如果要求外存上以ASCII代码形式存储,则需要在存储前进行转换。ASCII文件又称文本文件,每一个字节放一个字符的ASCII代码。
字符一律以ASCII码形式存储,数值型数据既可以用ASCII形式存储,也可以用二进制形式存储。如有整数10000,如果用ASCII码形式输出到磁盘,则在磁盘中占5个字节(每一个字符占一个字节),而用二进制形式输出,则在磁盘是只占用4个字节(用VC++时)。
文件缓冲区:
ANSI C标准采用“缓冲文件系统”处理数据文件,所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区。
从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去,如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量)。
文件类型指针:
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的有关信息(如文件的名字、文件状态及当前文件位置等)。这些信息是保存在一个结构体变量。该结构体类型是由系统声明的,取名为FILE。
声明FILE结构体类型的信息包含在头文件“stdio.h”中,一般设置一个指向FILE类型变量的指针变量,然后通过它来引用这些FILE类型变量。
- 打开与关闭文件
用fopen函数打开数据文件:
对文件读写之前应该“打开”该文件,在使用结束之后应“关闭”该文件。所谓打开是指为文件建立相应的信息区(用来存放有关文件的信息)和文件缓冲区(用来暂时存放输入输出的数据)。
在编写程序时,在打开文件的同时,一般都指定一个指针变量指向该文件,也就是建立起指针变量与文件之间的联系,这样就可以通过该指针变量对文件进行读写。所谓关闭,是指撤销文件信息区和文件缓冲区。
fopen函数的调用方式为:
fopen(文件名,使用文件方式);
FILE *fp;
fp=fopen("a1", "r");
#include <stdio.h>
int main()
{
FILE* fp;
fp = fopen(".\\text1.txt", "r");
if (fp == NULL)
{
printf("打开文件失败\n");
exit(1); // 终止程序
}
else
{
printf("打开文件成功\n");
fclose(fp);
}
return 0;
}
在打开一个文件时,通知编系统以下3个信息:需要访问的文件的名字、使用文件的方式、让哪一个指针变量指向被打开的文件。
读取 写入 添加 文本 r w a 二进制 rb wb ab 既可以读也可以写 r+ w+ a+ 如果打开失败,fopen函数将会带回一个出错信息,fopen函数将带回一个空指针NULL。使用exit退出
#include <stdio.h>
int main()
{
FILE* fp;
fp = fopen(".\\text2.txt", "r");
if (fp == NULL)
{
printf("打开文件失败\n");
exit(1);
}
else
{
printf("打开文件成功\n");
fclose(fp);
}
printf("未失败执行\n");
return 0;
}
计算机从ASCII文件读入字符时,遇到回车换行符,系统把它转换为一个换行符,在输出时把换行符转换称为回车和换行两个字符,在用二进制文件时,不进行这种转换,在内存中的数据形式与输出到外部文件中的数据形式完全一致,一一对应。
程序中可以使用三个标准的流文件:标准输入流、标准输出流、标准出错输出流。系统已对这三个文件指定了与终端的对应关系。标准输入流是从终端的输入;标准输出流是向终端的输出;标准出错输出流是当程序出错时将出错信息发送到终端。
程序开始运行时,系统自动打开这3个标准流文件。因此程序编写者不需要在程序中用fopen函数打开它们。所以我们以前用的从终端输入或输出到终端都不需要打开终端文件。
用fclose函数关闭数据文件,如果不关闭文件,将会丢失数据。
7.顺序读写数据文件
- 怎样向文件读写字符
读写一个字符的函数:
函数名 调用形式 功能 返回值 fgetc fgetc(fp) 从fp指向的文件读入一个字符 读成功,带回所读的字符,失败则返回文件结束标志EOF(即-1) fputc fputc(fp) 把字符ch写到文件指针变量fp所指向的文件中 写成功,返回值就是输出的字符,输出失败,则返回EOF(即-1)
#include <stdio.h>
int main()
{
FILE* fp;
char ch, FileName[20];
printf("请输入所用的文件名:\n");
scanf_s("%s", FileName, sizeof(FileName));
if ((fp = fopen(FileName, "w")) == NULL)
{
printf("无法打开此数据文件\n");
exit(1);
}
ch = getchar(); // 接收回车换行
printf("请输入字符串,以#号结束\n");
ch = getchar();
while (ch != '#')
{
fputc(ch, fp);
putchar(ch);
ch = getchar();
}
fclose(fp);
putchar(10);
return 0;
}
- 怎样向文件读写一个字符串
读写一个字符串的函数:
函数名 调用形式 功能 返回值 fgets fgets(str,n,fp) 从fp指向的文件读入长度为(n-1)的字符串,存放到字符数组str中 读成功,返回地址str,失败则返回NULL fputs fputs(str,fp) str所指向的字符串写到文件指针变量fp所指向的文件中 写成功,返回0,否则返回非0值
#include <stdio.h>
int main()
{
FILE* fp;
char str[300];
fp = fopen("test.txt", "r");
if (fp == NULL)
{
printf("打开文件失败\n");
exit(1);
}
else
printf("打开文件成功\n");
while (!feof(fp))
{
if (fgets(str, 300, fp) != NULL)
printf("读取的文件数据如下:\n%s\n", str);
}
fclose(fp);
return 0;
}
int main()
{
FILE* fp;
char* str = "How are you.";
fp = fopen("data.dat", "w");
if (fp == NULL)
{
printf("创建文件失败\n");
exit(1);
}
else
printf("创建文件成功\n");
fputs(str, fp);
fclose(fp);
return 0;
}
fgets函数的函数原型为:
char *fgets(char *str, int n, FILE *fp);
调用时可以写成
fgets(str, n, fp);
n是要求得到的字符个数,但实际上只读n-1个字符,然后在最后加一个'\0'字符,这样得到的字符串共有n个字符,把它们放到字符数组str中。
如果在读完n-1个字符之前遇到换行符'\n'或文件结束符EOF,读入即结束,但将所遇到的换行符'\n'也作为一个字符读入。
执行fgets成功,返回str数组的首地址,如果一开始就遇到文件尾或读数据错误,则返回NULL。
- 用二进制方式向文件读写一组数据
一般调用形式为:
fread(buffer,size,count,fp);
fwrite(buffer,size,count,fp);
buffer是一个地址,对fread来说,它是用来存放从文件读入的数据的存储区的地址;对fwrite来说,是要把此地址开始的存储区中的数据向文件输出。
size:要读写的字节数;
count:要读些多少个数据项;
fp:FILE类型指针。
#include <stdio.h>
#include <stdlib.h>
struct student
{
int no;
char name[10];
};
int main()
{
struct student st[2] = {
{101, "张三"},
{102, "李四"}
};
FILE* fp = fopen("student.txt", "w");
if (fp == NULL)
{
perror("Open File Fail.\n");
exit(1);
}
fwrite(st, sizeof(struct student), 2, fp);
fclose(fp);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
struct student
{
int no;
char name[10];
};
int main()
{
struct student st[2];
FILE* fp = fopen("student.txt", "r");
if (fp == NULL)
{
perror("Open File Fail.\n");
exit(1);
}
fread(st, sizeof(struct student), 2, fp);
printf("No:%d\tName:%s\n", st[0].no, st[0].name);
printf("No:%d\tName:%s\n", st[1].no, st[1].name);
fclose(fp);
return 0;
}
8.随机读写数据文件
- 随机读写数据文件
对文件进行顺序读写比较容易理解,也容易操作,但有时效率不高。随机访问不是按数据在文件中的物理位置次序进行读写,而是可以对任何位置上的数据进行访问,显然这种方法比顺序访问效率高得多。
- 文件位置标记及其定位
为了对读写进行控制,系统为每个文件设置了一个文件读写位置标记(简称文件标记),用来指示“接下来要读写的下一个字符的位置”。
一般情况下,在对文件进行顺序读写时,文件标记指向文件开头,进行读的操作时,就读第一个字符,然后文件标记向后移一个位置,在下一次读操作时,就将位置标记指向的第二个字符读入。以此类推,直到遇到文件尾,结束。
如果是顺序写文件,则每写完一个数据后,文件标记顺序向后移一个位置,然后在下一次执行写操作时把数据写入指针所指的位置。直到把全部数据写完,此时文件位置标记在最后一个数据之后。
可以根据读写的需要,人为地移动文件标记的位置。文件标记可以向前移、向后移,移到文件头或文件尾,然后对该位置进行读写——随机读写。
随机读写可以在任何位置写入数据,在任何位置读取数据。
- 文件位置标记的定位
可以强制使文件位置标记指向指定的位置,可以用以下函数实现:
1)用rewind函数使文件标记指向文件开头:
rewind函数的作用是使文件标记重新返回文件的开头,此函数没有返回值。
2)用fseek函数改变文件标记:
fseek函数的调用形式为:
fseek(文件类型指针,位移量,起始点);
起始点0代表“文件开始位置”,1为“当前位置”,2为“文件末尾位置”
位移量指以起始点为基点,向前移动的字节数。位移量应是long型数据(L)。fseek函数一般用于二进制文件。
fseek(fp,100L,0);
3)用ftell函数测定文件位置标记的当前位置:
ftell函数的作用是得到流式文件中文件位置标记的当前位置。由于文件中的文件位置标记经常移动,人们往往不容易知道当前位置,所以常用ftell函数得到当前位置,用相对于文件开头的位移量来表示。如果调用函数时出错(如不存在fp指向的文件),ftell函数返回值为-1L。
i = ftell(fp);
if (i == -1L) printf("error\n");
- 文件读写的出错检测
1)ferror函数:
ferror函数的一般调用形式为:
ferror(fp);
,如果返回值为0,表示未出错,否则表示出错。每次调用输入输出函数,都产生新的ferror函数值,因此调用输入输出函数后立即检查;调用fopen时,ferror的初始值自动置0。
2)clearerr函数:
作用是使文件错误标志和文件结束标志置为0。调用一个输入输出函数时出现错误(ferror值为非零值),立即调用clearerr(fp),使ferror(fp)值变为0,以便再进行下一次检测。
只要出现文件读写错误标志,它就一直保留,直到对同一文件调用clearerr函数或rewind函数,或任何其他一个输入输出函数。
#include <stdio.h>
int main()
{
FILE* fpold, * fpnew;
fpold = fopen("old.dat", "r");
fpnew = fopen("new.dat", "w");
while (!feof(fpold))
putchar(getc(fpold));
putchar(10); // 换行
rewind(fpold);
while (!feof(fpold))
putc(getc(fpold), fpnew);
fclose(fpold);
fclose(fpnew);
return 0;
}
标签:文件,专题,变量,int,函数,C语言,printf,1003,指针
From: https://www.cnblogs.com/lotuslaw/p/17022358.html