大一新生,最近刚学完c语言基础内容,想着赶紧把内容巩固一下,防止太长时间造成遗忘,如果有不足还请多多包涵
c语言的一些基础知识
格式说明符:
%d:以十进制形式输出整数。(%ld是输出长整型)
%o:以无符号的八进制形式输出整数。
%x:以无符号的十六进制形式输出整数。
%u:以无符号的十进制形式输出整数。
%c:以字符形式输出,只输出一个字符。
%s:输出字符串。
%f:以小数形式输出单、双精度数。(%lf输出双精度)
%e:以标准指数形式输出单、双精度数。
%g:选取%f或%e格式中输出宽度较短的一种格式。
此外,如果需要在printf语句和scanf语句中保留相关位数
可以使用以下格式说明符:
%m:用于printf语句中,可以指定保留小数的位数。例如,%02f表示保留两位小数,不足两位时前面补0。
%n:用于scanf语句中,可以指定读取的整数所占的位数。例如,%3d表示读取三个整数。
解引用操作符
\? : 在书写连续多个问号时使用,防止他们被解析成三字母词
\' :用于表示字符常量
\“ : 用于表示一个字符串内部的双引号
用于表示一个反斜杠,防止它被解释为一个转义序列符
\a : 警告字符,蜂鸣
\b : 退格符
\f : 进纸符
\n : 换行
\r : 回车
\t : 水平制表符
\v : 垂直制表符
\ddd: ddd表示1~3个八进制的数字。如:\130X
\xdd: dd表示2个十六进制数字。如:\x30 0
数据类型
数值类型:
(1)整数类型:
如byte、short、int、long,整数类型的存储空间大小不同,byte是8位,short是16位,int是32位,long是64位。
注意:
要注意的是unsiged和unsiged int 诸如此类的区别:是否能表示负数和无符号整数
- 范围: "unsigned int"只能表示非负整数,其范围是0到4294967295(2^32 - 1)。"int"通常可以表示正数、零和负数,其范围依赖于具体的实现(例如在32位系统中,其范围通常是-2147483648到2147483647)。
- 符号: "unsigned int"没有符号,所有的数都是正数或零。"int"有符号,可以表示正数、零和负数。
(2)浮点数类型:
如float、double,浮点数类型float是32位,用于存储带有小数点的数值,double是64位,用于存储更大范围的浮点数值。
(3)字符类型:
字符类型是char,用来存储单个字符。
(4)布尔类型:
布尔类型只有两个值true和false,用于表示真或假的逻辑判断
输入和输出
scanf: 输入一个数,格式为:
#include<stdio.h>
int main()
{
int a=0;//初始化
scanf("%d\n",&a);
return 0;
}
printf:输出一个数,格式为:
#include<stdio.h>
int main()
{
int a=0;//初始化
printf("%d\n",a)
return 0;
}
基本到这里,我们也就初步了解到了c语言的大概体系
接下来就开始了一些常见好用的语法
分支和循环
分支结构
分支结构允许程序根据特定的条件选择执行不同的代码块。C语言中的分支结构由if、else if和else,以及switch
关键字构成。
int num = 10;
if (num > 5)
{
printf("Number is greater than 5\n");//如果数大于5,从这里出去
}
else if (num == 5)
{
printf("Number is equal to 5\n");//如果数等于5,从这里出去
}
else
{
printf("Number is less than 5\n");//如果数小于5,从这里出去
}
switch (dayOfWeek)
{
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
case 4:
System.out.println("Thursday");
break;
case 5:
System.out.println("Friday");
break;
case 6:
System.out.println("Saturday");
break;
case 7:
System.out.println("Sunday");
break;
default:
System.out.println("Invalid day");
break;
}
循环结构
循环结构允许程序重复执行特定的代码块。C语言中的循环结构由for
、while
和do...while
关键字构成。
int i=0;
for(i=0;i<10;i++) //for循环
{printf("%d ",i);//输出结果为0 1 2 3 4 5 6 7 8 9
}
int i = 1;
int sum = 0;
while (i <= 10) //while循环{
sum += i;
i++;
}
printf("Sum = %d\n", sum);
int i = 1;
int sum = 0;
do //do while循环{
sum += i;
i++;
} while (i <= 10);
printf("Sum = %d\n", sum);
do 循环和 while 循环两者相比,do循环是先运行一次循环再判断,而while循环则是先判断后循环
基本上,分支和循环学完了之后,就基本掌握了c语言的语法
接下来就可以开始学习一些相关的重要知识了,期间很运用到很多分支循环语句在其中
二分查找
二分查找也被称为折半查找,它是一种在有序列表中进行查找的高效方法。
在进行二分查找时,首先需要确定列表是已排序的。然后,您可以选取列表的中间元素,如果选取的元素正好是要查找的元素,则查找过程结束。如果选取的元素大于要查找的元素,则说明要查找的元素可能位于列表的左半部分,因此您可以在左半部分继续进行查找。如果选取的元素小于要查找的元素,则说明要查找的元素可能位于列表的右半部分,因此您可以在右半部分继续进行查找。这个过程会一直重复,直到找到要查找的元素,或者搜索范围为空。
二分查找主要适用于已排序的列表,对于未排序的列表,需要先进行排序再进行查找,否则结果可能不准确。
其实,二分查找很像高中时候学过的二分法,就是每次都砍掉一半。
例如:数组内包含十个数1 2 3 4 5 6 7 8 9 10
数组下标是0 1 2 3 4 5 6 7 8 9
第一次查找,就在5和6之间分开,然后往要找的地方进行查找,如要找的是3,就在[0]--[4]之间继续查找
第二次查找,就直接找到[2],这个时候我们已经找到了想要的数组3
若是数组的容量再大一点,就需要再次进行多次查找
当然,这种方法无疑是比起一个一个从头到尾进行检索来的快,大大提高了代码运行的效率。
一个优秀的程序员,不仅需要掌握扎实的基础知识,在另一方面还要学会设计代码,找到最优解法,这才是一个程序员该有的良好素质
goto
这是一个很危险的语法,goto虽然可以用来应用于嵌套式的if语句,直接使循环终止,很方便
But有一点需要注意,如果分装过多的goto语句会让程序编写更加崩溃,从而导致程序后期修复的复杂性加大
continue
终止这一次循环,但仍然执行下一次循环
break
终止这个循环
以上就是语法的大致内容!
接下来就到来了c语言很重要的一个部分:
指针
内存单元的编号==地址==指针
指针变量:&
注:解引用操作符*
*pa就是通过pa中存放的地址,找到指定的空间,*pa就变成了a变量
指针变量的大小:
指针类型是有意义的——>指针类型决定了指针进行+1/-1操作的时候,一次跳过几个字节
+n/-n跳过的是n个指向值
指针类型决定了指针再解引用操作时的权限,也就是一次解引用访问几个字节
char* 类型表示访问一个字节
int* 类型表示访问四个字节
指针类型的特点如何使用
数组:
1 2 3 4 5 6 7 8 9 10
1、数组下标的方式访问
int arr[]={1,2,3,4,5,6,7,8,9,10};
int i=0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0;i<sz;i++)
{
printf("%d ",arr[i]);
}
2、指针的方式访问
int arr[]={1,2,3,4,5,6,7,8,9,10}
int i=0;
int sz=sizeof(arr)/sizeof(arr[0]);
int *p =&arr[0];
for(i=0;i<sz;i++)
{
printf("%d ",*p);p=p+1;
}
接下来介绍const来修饰指针
1、const放在*的左边(限制的是*p)限制的是指针指向的内容,不能通过指针来修改指针指向的内容,但是不可以修改指针变量的指向
const int *p 或者是 int const *p
2、const放在*的右边(限制的是p)限制指针变量本身,不能修改指针变量的指向,但可以修改指针指向的内容
int * const p
const修饰变量,使这个变量不能被修改,一旦修改就会报错
指针运算
指针+-整数(类似于连续下标,例:p+1)
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//把数组arr初始化为1
//写法1:
/*for (i = 0; i < sz; i++)
{
arr[i] = 1;
}*/
//写法2:
/*int* p = arr;
for (i = 0; i < sz; i++)
{
*p = 1;
p++;
}*/
//写法3:
int* p = arr;
for (i = 0; i < sz; i++)
{
*(p + i) = 1;
}
return 0;
}
指针-指针【地址- 地址】(有前提的)
#include<stdio.h>
#include<string.h>
//方法1:
int my_strlen(char* str)
{
int count = 0;
while (*str != '\0')
{
count++;
str++;
}
return 0;
}
//2.递归的方法
//3.指针-指针
int my_strlen(char* str)
{
char* start = str;
while (*str != '\0')
{
str++;
}
return (str - start);
}
int main()
{
int len = my_strlen("abcdef");//传过去的是a的地址
printf("%d\n", len);
return 0;
}
前提:两个指针指向同一块空间的
得到的值的绝对值,是指针和指针之间元素的个数
指针的关系运算(比较大小)
野指针
野指针的出现(指针指向的位置是不可知的、随机的、没有明确限制的)
成因:
1、指针未初始化
2、指针越界访问
3、指针指向的空间释放
如何规避野指针:
1、指针初始化(明确指针指向,给指针赋值;如果不知道,就赋值为NULL)[NULL为空指针]
2、小心指针越界
3、指针变量不再使用时,及时置NULL,指针使用之前检查有效性
再使用指针之前,进行有效判断!!!
可以使用assert断言
#include<assert.h>头文件定义宏assert()
若要关闭assert可以在前面定义一个宏
#define NDEBUG
缺点:运算时间长
指针的使用和传值调用
***区分传值调用和传址调用***
传址调用:
将变量的地址传递给函数,这种函数调用的方式叫做传址调用
void Swap(int * px,int * py){
int z=0;
z=*px;
*px=*py;
*py=z;
}
传值调用:
int a=10;
int b=20;
int m=max(a,b);
printf("%d\n",,m);
int max(int x,int y){
if(x>y)
return x;
else
return y;
}
数组名
对于数组名的理解:数组名是数组首元素地址,但是这里有两个例外
1、sizeof(数组名),这里的数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小
2、&数组名,这里的数组名 表示整个数组,&数组名:取出的是整个数组的地址
除此之外,遇到的所有数组名都是数组首元素的地址
int main()//使用指针访问数组
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++) {
printf("%d ", *p + 1);
printf("%d ", arr + i);
printf("%d ",arr[i]);
printf("%d ", p[i]);
arr[i] == *(arr + i);
*(p + i) == p[i];
arr[i] == p[i];
arr[i] == *(arr + i) == *(i + arr) == i[arr];
}return 0;
}
一维数组传参的本质:
在函数内部无法得到数组的元素个数,本质上数组传参的本质上传递的是数组首元素的地址
总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式
冒泡排序
思想:两两相邻的元素进行比较
#include<stdio.h>
void Bubble_sort(int arr[], int size)
{
int j,i,tem;
for (i = 0; i < size-1;i ++)//size-1是因为不用与自己比较,所以比的数就少一个
{
int count = 0;
for (j = 0; j < size-1 - i; j++) //size-1-i是因为每一趟就会少一个数比较
{
if (arr[j] > arr[j+1])//这是升序排法,前一个数和后一个数比较,如果前数大则与后一个数换位置
{
tem = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tem;
count = 1;
}
}
if (count == 0)//如果某一趟没有交换位置,则说明已经排好序,直接退出循环
break;
}
}
int main()
{
int arr[10];
int i;
printf("请输入10个数\n");
for (i = 0; i < 10; i++) //接收用户的数值
{
scanf("%d", &arr[i]);
}
printf("排序前的数组>");
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n排序后的数组>");
Bubble_sort(arr, 10);
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
冒泡排序的思想很重要,希望可以多多研究
二级指针
指针变量也是变量,是变量就有地址
指针数组:存放指针的数组(指针数组的每个元素都是用来存放地址(指针)的)
类比:整型数组-存放整型数组
字符数组-存放字符的数组
int* char* short*
指针变量
1、字符指针变量
2、数组指针变量(和指针数组不同)
指针数组是数组,用于存放指针的数组
数组指针
类比:
字符指针:指向字符的指针,存放的是字符的地址
char ch=‘ w ’ ; char *pc=& ch ;
整型指针:指向整型的指针,存放的是整型的地址
int n =100; int *p=&n;
数组指针:指向数组的指针,存放的是数组的地址
int arr[10]; int(*p)5[10]=&arr //注意:int *p[10]是指针数组
二维数组传参的本质
二维数组也是数组,二维数组的数组名也是数组首元素的地址
(1)二维数组在内存中也是连续存放的
(2)二维数组的数组名也是数组首元素的地址
二维数组的首元素是谁??
二维数组其实是一维数组的数组,二维数组数组名表示首元素地址,也就是第一行的地址
函数
函数指针变量
指针数组:int* arr[10]
数组指针是指针——int arr[10]; int (*pa)[10]=&arr;
函数指针是指针——是指向函数的指针,是存放函数地址的指针!!!
int get_max(int x, int y)
{
if (x > y) //int(*p1)(int ,int)=get_max;函数指针变量
return x; //int(*p2)(int ,int)=&get_max;函数指针变量
else
return y;
}
关键字typedef
typedef unsigned int uint; //将unsigned int 简化为uint
注意:typedef void(*pf)(int)改写的pf要放在*旁边才可以简化成为pf
函数指针数组
整型指针数组:是数组,数组中存放的都是整型指针
函数指针数组:是数组,数组中存放的都是函数指针
转移表
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int del(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}void menu()
{
printf("**************************\n");
printf("***** 1.Add 2.Del *****\n");
printf("***** 3.Mul 4.Div *****\n");
printf("***** 0.exit *****\n");
printf("**************************\n");
}int main()
{
int input = 1;
int a = 0;
int b = 0;
int ret = 0;
int (*p[5])(int, int) = { 0,add,del,mul,div };
while (input)
{
menu();
printf("请输入:>");
scanf("%d", &input);
if (input <= 4 && input >= 1)
{
printf("请输入两个整数:>");
scanf("%d%d", &a, &b);
ret = (*p[input])(a, b);
printf("结果为:%d\n", ret);
}
else
if (input == 0)
{
printf("退出程序!\n");
printf("\n觉得不错别忘了点赞三连支持欧o(>ω< )o!!!\n");
printf("(深情)\n");
}
else
{
printf("选择错误,请重新选择!\n");
}
}
return 0;
}
回调函数qsort
回调函数就是一个通过指针调用的函数
注:排序的算法很多
例如:冒泡排序,选择排序,插入排序,希尔排序,快速排序
qsort使用:使用该函数可以用来进行快速排序的思想排序数据
qsort是一个库函数,用来对数据进行排序,可以对任意类型的数据
qsort函数有四个参数
void qsort (void* base, //base指向待排序的第一个元素
size_t num, //num是待排序的元素个数
size_t size, //待排序的元素个数大小
int(*compar)(const void* e1,void* e2)
);
//compar是一个函数指针,指向的函数可以比较两个元素
void* 是无具体类型的通用类型指针
void* 类型的指针变量,可以接受任何数据类型的地址
[pv+1]、[*pv]都不可以
strcmp
是库函数,用来专门比较两个字符串的大小
int strcmp(const char* s1,const char* s2)
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字
sizeof和strlen的对比
sizeof
sizeof是操作符,计算的是 变量所占内存空间的大小,单位是字节
sizeof不在乎内存中的数据,在计算大小的时候是根据类型去推算的
size_t其实是专门设计给sizeof的,表示sizeof的返回值类型
int a = 0;
printf("%zd", sizeof (a));
printf("%zd", sizeof a);
printf("%zd", sizeof (int));
以上三种都可以表示,sizeof的操作数如果是一个表达式,表达式不参与计算
int n=sizeof (s=i+4);
strlen
strlen是库函数,需要包含<string.h>
strlen仅仅是求字符串的长度,并且关注里面有没有\0
他会从str开始,往后一直查找直到找到\0为止才结束(有可能出现越界查找的可能)
【此时编译器会发疯!!!】
char arr[] = "abcdef";
printf("%d", strlen(arr));
//strlen的返回信息是size_t
字符函数和字符串函数
字符分类函数
专门做字符分类的,也就是一个字符属于什么类型的字符
函数: 如果他的参数符合下列条件就返回真
iscntrl 任何控制字符
isspace 空白字符:空格' '、换页'\n'、回车'\r'、制表符'\t'或者垂直制表符‘\v’
isdigit 十进制数组0~9
isxdigit 十六进制数字,包括所有十进制数组,小写字母a~f,大写字母A~F
islower 小写字母a~z
isupper 大写字母A~Z
isalpha 字母a~z或者A~Z
isalnum 字母或者数字,a~z,A~Z,0~9
ispunct 标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph 任何图形字符
isprint 任何可打印字符,包括图形字符和空白字符
例如:islower是用来判断参数是否是小写字母的,如果是小写字母,就返回非0的值,如果不是就返回0
字符换转函数
tolower 将大写转换成小写字母
toupper 将小写转换成大写字母
字符分类函数和字符转换函数需要包含一个库函数——#include<ctype.h>
字符串函数
strlen:求字符串长度的函数
strcpy:字符串拷贝函数
char* strcpy(char* s1,const char* s2)
strcat:字符串追加
char* strcat(char* s1,const char* s2)
strcmp:字符串比较,用来比较两个字符串大小关系的
(比较两个字符串中对应位置上的字符)1>2返回大于0 1=2返回0 1<2返回小于0
int strcmp(char* s1,const char* s2)
以上三个是长度不受限制的字符串函数
strncpy:拷贝几个字符串
char* strncpy (char* s1,const char* s2, 3 )//第三个数是拷贝的数量
strncat:追加要看有多少个可追加,够不够追加
char* strncat (char* s1,const char* s2,3)
strncmp:比较几个字符串
int strncmp(char* s1,const char* s2.3)
以上三个是长度受限制的字符串函数
strstr:查找str2在str1字符串中,并返回str2在str1中第一次出现的位置,如果没有找到就返回一个空指针NULL
const char* strstr(const char* str1,const char* str2)
strtok
char* strtok(char* s1.const char* s2)
1、第一个参数指定一个字符串,它包含了多个0或者多个有sep字符串中一个或者多个分隔符分割的标记,strtok函数找到str的下一个标记,并将其用\0结尾,返回一个指向这个标记的指针
(strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串,一般都是临时拷贝的内容且可被修改)
2、strtok函数的第一个参数不为NULL,函数找到str中的第一个标记,strtok函数将保存它在字符串中的位置
3、strtok函数的第一个参数为NULL,函数将在同一个字符串中被保存的位置开始,查找下一个标记,如果字符串不存在更多标记,则返回NULL指针
strerror:返回一个错误码所对应的错误信息字符串的起始地址
0:No error
1:Operation not permitted2:No such file or directory
3:No such process
4:Interrupted function call5:Input/output error
6:No such device or address7:Arg list too long
8:Exec format error
9:Bad file descriptor
404 --> 错误码
perror:主动分析代码,找到错误信息之后,打印错误信息,相当于strerror+printf
内存函数
内存函数是针对内存块的,不在乎内存内的数据
memcpy
void* memcpy(void* s1, const void* s2,size_t num);
memcpy是做内存拷贝的,拷贝的可能是字符串,也可能是整型数组,也可能是结构体,或者其他的类型
memcpy可以处理不重叠的内存之间的数据拷贝
如果内存之间有重叠,要进行数据拷贝,可以进行memmove函数
memmove
void* memmove (void* s1,const void* s2,size_t num);
注:为了方便可以都使用memmove
memset
把指定的字节设置成所要的字节,但是每个整型却做不到
memset是以字节为单位来设置字节的
void* memset (void* ptr ,int value,size_t num)
memcmp
对内存进行比较
int memcmp (const void* ptr1,const void* ptr2, size_t num )
数据在内存中的存储
操作符
在操作符中,有和二进制有关的操作符:& | ^ >> <<
注:操作符都是整数
表示方法有三种:原码、反码、补码
最高位叫做符号位,符号位为1表示为负数,0则为正数
正整数的原码、反码、补码相同;
负整数的原码、反码、补码不相同(反码的符号位不变,其他的取反;补码是反码+!)
对于整数来说,数据存放内存中其实存放的是补码
数据在内存中存储的是二进制,而在vs2022中展示的是十六进制,而且存储的数据是倒着放的
扩展:大小端字节和字节序的判断
当一个数组超过一个字节的时候,存储在内存中就有存储的顺序问题
内存中的存储单位是一字节的
大端字节序:表示的是字节的顺序
将一个数据或者一个数值的低位字节序的内容存储到高地址处,高位字节序的内容存储到低地址处
小端字节序:表示的是字节的顺序
将一个数据或者一个数值的低位字节序的内容存储到低地址处,高位字节序的内容存储到高地址处
对于编译器的模式,需要我们自己进行判断
#include <stdio.h>
int main()
{
int i = 1;
int temp;
temp = *(char*)&i;
temp == 1 ? printf("小端") : printf("大端");
return 0;
}
浮点数在内存中的存储
signed char:-128~127
unsigned char:0~255
signed short:-32786~32786
unsigned short:0~65535
存储内存的过程
32位的浮点数,最高1位存储符号位s,接着存储8位存储指数e,剩下的23位存储有效数字
64位的浮点数,最高1位存储符号位s,接着存储11位存储指数e,剩下的52位存储有效数字
取出内存的过程
主要掌握:
1、e不全为0或者不全为1(正常表示即可)
2、e全为0(表示+/- 0,也是无限接近与0的数)
3、e全为1(表示+/- 无穷大)
自定义类型
自定义类型就是自己定义用来方便表示的类型:有结构体、枚举、联合体等
结构体
可以包含多种类型的数据,结构体是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量
结构的声明
struct name(结构体名字)
{
member-list;(可以有多个成员)
}variable-list;(这里bariable-list也可以把上面的name进行缩写,方便后续函数的编写)
例如:
struct Membercompary
{
char name[20];
int age;
float score;
}Mc;
匿名结构体
如果没有对结构体类型重命名的话,基本就只能使用一次
可以理解为一个饭盒,每次只能打包一次,后面就不能再用了
struct S
{
int a;
char c;
float d;
}s={0};
结构体的自引用
struct node
{
int data;//存数据-4
struct node* next;//下一个节点的地址
}
int main()
{
struct node n={0};
return 0;
}
结构体内存对齐
对齐规则:
1、结构体的第一个成员对齐到和结构体起始位置偏移量为0的地址处
2、其他成员变量要对其到某个数字(对齐数)的整数倍的地址处
对齐数:编译器默认的一个对齐数 与 该成员变量大小的较小值
vs中默认的值为8,而Linux中没有默认对齐数,对齐数就是成员自身大小
3、结构体总大小为最大对齐数(结构体中每个成员都有一个对齐数,所有对齐数中最大的)的整数倍
4、如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
offsetof()
是一个宏,用来计算结构体成员,相较于起始位置的偏移量
offsetof(结构体类型,成员)
需要包含头文件:#include<stddef.h>
为什么存在内存对齐???
1、平台的原因:由于各平台的可移植性不同,造成的差异
2、性能问题:可能与主机的处理器有关
总体来说:结构体的内存对齐是那空间换取时间的做法,为了节省内存空间,可以把相同类型的成员排到一起,减少内存的浪费
修改默认对齐数
#pragma pack(1)//设置默认对齐数为1
#pragma pack( )//取消默认对齐数,还原为默认值
结构体实现位段
结构体如何实现位段??
位段是基于结构体的,它的出现就是为了节省空间,方便内存管理
位段的成员必须是int、unsigned或者signed int 、char等类型
位段的成员名后边有一个冒号和一个数字:int _a:2;
struct S
{
int _a;
int _b; //普通结构体
int _c;
int _d;
}
结构体实现位段:(二进制位0)
struct S
{
int _a:2;
int _b:5; //后面的数字指的是二进制位,表示占用两个比特位
int _c:10;
int _d:30;
}
位段的内存分配
1、位段的成员必须是int 、unsigned int或者signed in、char等类型
2、位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
3、位段涉及很多不确定因素,位段是不跨平台的,注重可移植程序应该避免使用位段
(所以上面的只是一个介绍)
当开辟了内存之后,内存中的每个比特位从左向右还是从右向左使用,这个不确定
当前面使用的时候,剩余的空间不足下一个成员使用的时候,剩余的空间是否使用,这个不确定
位段的跨平台问题
int位段被当成有符号数还是无符号数是不确定的
位段中最大位的数目不能确定(16位机器人最大16,32位机器最大32,写成27在16位机器中会发生问题)
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
当一个结构体包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这个是不确定的
注意:不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段成员输入值了,因此只能先输入放在一个变量中,然后赋值给位段的成员
联合体类型的声明
像结构体一样,联合体也是由一个或者多个成员构成的,这些成员可以是不同的类型
但是编译器只为最大的成员分配足够的内存空间,联合体的特点是所有成员公用同一块内存空间,所有的联合体也叫做:共同体
给联合体其中一个成员赋值,其他成员的值也跟着变化(在同一个时间点上只能使用一个成员)
联合体大小的计算
联合的大小至少是最大成员的大小
当最大成员的大小表示最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍
联合体的应用是相对苛刻的,只在需要的时候会用到
动态内存管理
众所周知空间开辟出来的大小是固定的,因为数组在声明的时候,必须指定数组的长度,数组空间一旦确定大小就不能调整
接下来就开始掌握第一个开辟内存空间的函数
malloc
malloc必须包含一个头文件
#include<stdlib.h>
void* malloc ( size_t size );
如果开辟成功,则返回一个指向开辟好空间的指针
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查
返回值类型是void* 的时候,malloc函数不知道开辟空间的类型,具体在使用者来决定类型
注意:不能开辟0内存的空间(除非你吃饱了没事干)
那么有一个问题来了,malloc申请的空间要如何回收呢?
因为我们都知道,内存不可能一直放在那里不用,我们一定会想办法把他释放掉
所以有以下方法:
1、用free回收,这个操作可以让你在编写代码的时候节省空间
2、如果自己不释放的时候,程序结束后也会有操作系统回收
(但是,假设是一个服务器,一直在运行,有可能结束程序吗?)
3、malloc是在堆区上申请内存
free
void free ( void* ptr );
注意:free只能释放动态内存
那我们为什么要释放内存?因为内存申请了不使用后,会造成内存泄漏,因此我们必须时刻注意
calloc
void* calloc (size_t num, size_t size );
realloc
void* realloc (void* ptr,size_t size )
realloc是用来调整内存空间的
例如:以20个字节开辟到40个字节空间
1、后面空间够开辟的话,就在后面继续开辟一块20个字节同样大小的空间
2、后面空间不够,就在别的地方开辟一块40字节大小的空间
旧的空间会被自动释放,并返回新的地址
常见的动态内存错误
1、对空指针NULL的解引用操作
malloc calloc realloc
在开辟或调整,空格失败的时候返回NULL
2、对动态开辟空间进行越界访问
3、对非动态开辟内存使用free释放
4、使用free释放一块动态开辟内存的一部分
5、对同一块动态内存多次释放
6、动态开辟内存忘记释放(内存泄漏)
切记:动态开辟的内存空间一定要释放
柔性数组
柔性数组,是一个结构体成员。在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组,用来配合动态内存管理
柔性数组的特点:
1、结构中的柔性数组成员前面必须至少一个其他成员
2、sizeof返回的这种结构大小不包括柔性数组的内存
3、包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
柔性数组更有利于访问数组,也方便内存释放:coolshell
C语言结构体里的成员数组和指针
C/C++程序内存分配的几个区域:
1、栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元会被自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限,栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等
2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表
3、数据段(静态区)(static):存放全局变量、静态数据,程序结束后由系统释放
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码
文件操作
数据存储内存中:如果程序退出,会导致数据丢失
数据存储在硬盘中:其实就是存储在文件中,数据就不会因为程序退出就丢失
想要数据持久化保存的话,就要存储到文件当中
文件名
一个文件名要有一个唯一的文件标识,以便用户识别和引用
文件名包含三个部分:文件路径+文件名主干+文件后缀
为了方便起见,文件标识常常被称为文件名
文件名的后缀是可以随便设定的,但是会默认会由一些常见的后缀:.txt .doc
接下来,让我们一起好好探讨一下文件吧!
磁盘上文件是文件,但是在程序设计中,一般谈到的文件有两种:程序文件和数据文件
程序文件
包括源程序文件(后缀为.c)
目标文件(windows环境后缀为.obj)
可执行程序(windows环境后缀为.exe)
数据文件
文件内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上
其实有时候外面会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件
根据数据的组织形式,数据文件又被称为文本文件或者二进制文件
文本文件
以ASCII码值存入
二进制文件
以二进制形式存入
流
流是一个抽象出来的概念
标准流
C语言程序再启动的时候,默认打开了三个流
stdin
标准输入流,大多数环境中从键盘输入,scanf函数就是从标准输入流中读取数据
stdout
标准输出流,大多数环境中输出至显示器界面,printf函数就是将信息输出到标准输出流
stderr
标准错误流,大多数环境中输出到显示器界面
stdin、stdout、stderr三个流的类型是:FILE*,通常称为文件指针
C语言就是通过FILE*的文件指针来维护流的各种操作的
文件指针
缓冲文件系统中(c盘、d盘),关键的概念“文件类型指针”,简称“文件指针”
在每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息
这些信息是用来保存在一个结构体变量中,该结构体类型是由系统声明的,取名为FILE
FILE* pf //文件指针变量
可以通过文件指针变量来找到与他相关联的文件
文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束后关闭文件
在编写程序的时候,再打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系
ANSI C 规定使用fopen函数来打开文件,fclose函数来关闭文件
(1)打开文件
FILE* "指针" = FILE* fopen ( const char* filename ,const char* mode);
(2)关闭文件
int fclose (FILE* stream );
指针
打开方式
“r”
文件必须存在,文件如果不存在,则打开失败
fopen函数打开失败会返回空指针,如果打开成功,则会返回文件信息区的地址
“w”
写文件,如果文件存在,则会清空原来文件的内容,相当于变成了空文件;如果文件不在村,会新建一个空文件
“a”
向文本件尾添加数据,如果文件不存在则会建立一个新的文件
“rb”
只读,为了输入数据,打开一个二进制文件,如果不存在则出错
“wb”
只写,为了输出数据,打开一个二进制文件,如果不存在则建立一个新的文件
“ab”
追加,向一个二进制文件尾添加数据,如果不存在则建立一个新的文件
“r+”
读写,为了读和写,打开一个文本文件,如果不存在则出错
“w+”
读写,为了读和写,建立一个新的文件,如果不存在则还是会建立一个新的文件
“a+”
读写,打开一个文件,在文件尾进行读写,如果不存在则还是会建立一个新的文件
“rb+”
读写,为了读和写,打开一个二进制文件,如果不存在就出错
“wb+”
为了读和写,建立一个新的二进制文件,如果不存在就建立一个新的文件
“ab+”
读写,打开一个二进制文件,在文件尾进行读和写,如果不存在就建立一个新的文件
绝对路径和相对路径
什么是绝对路径和相对路径???
在我们平时使用计算机时要找到需要的文件就必须知道文件的位置,而表示文件的位置的方式就是路径
绝对路径是指文件在硬盘上真正存在的路径,可以从任何地方访问。
而相对路径是指相对于当前文件所在的位置的路径,只能在当前文件所在的目录或其子目录中访问
在使用相对路径时,可以使用“.”表示当前目录,“…”表示上级目录
在将绝对路径转换为相对路径时,可以忽略两个文件路径中相同的部分,只需要考虑它们不同的部分即可
1、.表示当前路径
2、..表示上一级路径
但是输入的时候要注意的是:.\\..\\...\\
(3)读写文件
顺序读写函数
fgetc 字符输入函数 所有输入流 一次读取一个字符
fputc 字符输出函数 所有输出流 一次写一个字符
fgets 文本行输入函数 所有输入流 一次读取一行数据
fputs 文本行输出函数 所有输出流 一次写一行数据
fscanf 格式化输入函数 所有输入流
fprintf 格式化输出函数 所有输出流
fread 二进制输入 文件
fwirte 二进制输出 文件
注意:上面说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流)
所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)
printf:针对标准输入流(stdin)的格式化输入函数
scanf:针对标准输出流(stdout)的格式化输出函数
fprintf:针对所有输入流的格式化输入函数
fscanf:针对所有输出流的格式化输出函数
sprintf:从字符串中读取格式化的数据
sscanf:把格式化的数据转化成字符串
随机读写:虽然是考上面顺序指针的函数来完成的
fseek:根据文件指针的位置和偏移量来定位文件指针
ftell:返回文件指针相对于起始位置的偏移量
rewind:让文件指针的位置回到文件的起始位置
文件读取结束的判定
1、被错误使用的feof
在文件读取的过程中,不能使用feof函数的返回值直接来判断文件是否结束
feof的作用是:当文件读取结束的时候,判断读取结束的原因是否是因为遇到文件尾结束的
(1)文本文件读取是否结束,判读返回值是否为EOF(fgetc),或者NULL(fgets)
fgetc用来判断返回值是否为EOF
fgets用来判断返回值是否为NULL
(2)二进制文件的读取结束判断,判断返回值是否小于实际要读的个数
例如:fread判断返回值是否小于实际要读的个数
编译和链接
翻译环境和运行环境
在ANSI C的任意一种实现中,存在两个不同的环境
第一种是翻译环境,在这个环境中源代码被转换成可执行的机器指令(二进制指令)
第二种是执行环境,它用于实际执行代码
翻译环境
翻译环境中分为两个部分:编译和链接
在windows:目标文件的后缀为.obj 可执行文件的后缀是.exe
在Linux:目标文件的后缀是.o 可执行文件没有后缀
预处理
将所有的#define删除,并展开所有的宏定义,接着处理所有的条件编译指令(含#include的),将包含的头文件内容插入到该预编译指令的位置。
这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件
紧接着,删除所有的注释,添加行号和文件名标识,方便后续编译器生成调试信息,或者保留所有的#pragma的编译器指令,编译器后续会使用
经过预处理后的.i文件不再包含宏定义,因为此时的宏已经被展开,并且包含的头文件都被插入到.i文件当中
所以我们无法知道宏定义或者头文件是否包含正确的时候,可以查看预处理后的.i文件来确认
编译
编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,生成相关的汇编代码文件
1、词法分析(将代码拆开)
2、语法分析(生成一个语法树)
3、语义分析及优化(分析语法的正确性)
链接
链接是一个复杂的过程,链接的时候需要把一堆文件链接到一起,才可以生成可执行程序
其过程主要是:地址和空间分配,符号决议和重定位等这些步骤
链接解决的是一个项目中多文件、多模块之间的互相调用的问题
运行环境
1、程序必须载入内存中,再有操作系统的环境中,一般这个由操作系统来完成
在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
2、程序的执行便开始,接着便开始调用main函数
3、开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址,程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程会一直保留他们的值
4、终止程序,正常终止main函数,也可能是意外终止
预处理
常见宏定义
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则为未定义
#define 定义宏
格式:#define + 名字(参数) + 内容
注意:名字和参数中间必须紧联
带有副作用的宏参数
当宏参数在宏定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这宏的时候,就可能出现危险,导致不可预测的结果,副作用就是表达式求值的时候出现的永久性效果
例如:x+1 //没有副作用
x++ //有副作用
宏替换的规则
在程序中扩展#define 定义符号和宏时,需要设计几个步骤
1、在调用宏的时候,首先对参数进行检查,看看是否包含任何由#define 定义的符号,如果是,他们首先会被替换
2、替换文本随后被插入到程序中原来文本的位置,对于宏,参数名被他们的值所替换
3、最后,再次对结果文件进行扫描,看看他是否包含任何由#define 定义的符号,如果是,就就重复上述处理过程
注意:
1、宏参数和#define 定义中可以出现其他#define 定义的符号,但是对于宏,不能出现递归(也就是不能自己调用自己)
2、当预处理搜索#define 定义的符号时,字符串常量的内容并不被搜索
宏函数的对比
优势:宏通常被应用于执行简单的运算
1、宏比函数在程序规模和速度方面更胜一筹
2、宏的类型是无关的
劣势:
1、每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度
2、宏是没法调式的
3、宏由于与类型无关,也不够严谨
4、宏可能会带来运算符优先级的问题,导致程序容易出现错误
但是尽管如此,宏有时候可以做到函数做不到的事情,比如宏的参数可以出现类型,但是函数做不到
计算逻辑如果比较简单,就可以使用宏
如果计算逻辑比较复杂, 就建议使用函数
#和##
是预处理中的运算符
#运算符将宏的一个参数转换成字符串字面量,它仅允许出现在带参数的宏的替换列表中
#运算符所执行的操作可以理解为“字符串化”
#a 就是“a”
##可以把位于他两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
##被称为记号粘合,这样的连接必须产生一个合法的标识符,否则其结果就是未定义的
命名约定
一般来讲,函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者
那我们平时就要养成一个习惯:把宏名全部写成大写,函数名不要全部大写
#undef
移除一个宏定义
命令行定义
许多c的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程
条件编译
在编译一个程序的时候,我们如果将一条语句(一组语句)编译或者放弃是很方便的,因为我们由条件编译指令
1、满足条件就编译
2、不满足就放弃编译
常见的条件编译指令
(1)判断
#if 常量表达式:常量表达式不能有变量
//……
#endif
(2)多个分支
#if 常量表达式
//……
#elif 常量表达式
//……
#else
//……
#endif
(3)判断是否被定义
#if defined (max)
//……
#endif
#if !defined (max) ifndef(max)
//…… //……
#endif #endif
(4)嵌套指令
#if defined (OS1)
#ifdef OPTION 1
unix1();
#endif
#ifdef OPTION 2
unix2();
#endif
#elif defined (OS2)
#ifdef OPTION 2()
msdos();
#endif
#endif
头文件的包含
本地文件的包含:自己定义的头文件的包含
#include“filename”
查找策略:现在源文件所在的目录下查找,如果该头文件未找到,编译器就像查找库函数文件一样在标准位置查找头文件
如果找不到,就提示编译错误(查找速度慢)
库文件的包含
#include <stdio.h>
查找头文件直接去标准路径下查找,如果找不到就提示编译错误
其他预处理指令
#error
#pragma
#line
……
#pragma pack()
c++后引入一个函数:内联函数inline
内联函数具有了函数的特点,也具有了宏的特点