正数和负数在内存中都是以补码的形式存储的,但不同的是正数的原码,补码,反码都是相同的,而负数的原码,补码和反码是不同的。
负数的原码,补码,反码之间存在什么关系?
补码等于原码按位取反,但最高位即符号位不变
反码等于原码加一
原码等于反码按位取反加一,也等于反码减一得到补码再按位取反
注意:符号位0表示正数,1表示负数
例如:
由此我们可以去理解位操作符^在计算机中是怎么运算的
如下代码
#include
int main() {
int a = 10;
int b = -12;
int c = a ^ b;
printf("a=%d b=%d\n", a, b);
printf("a^b=%d\n", c);
return 0;
}
^操作符成为按位异或操作符,计算方式是以补码按位比较,当相同时结果为0,不同时结果为1
关键点在于数据在进行操作时,在计算机内都是以补码的形式进行操作,而输出时是以原码的形式输出,而在对正数进行操作时,因为正数的原码,补码,反码都相同,所以你可能会没考虑到它也是以补码的形式被操作的
注意:在参与异或运算时符号位也参与,而在负数求补码时符号位不参与即不变
例二:-12^-10
#include
int main() {
int a = -10;
int b = -12;
int c = a ^ b;
printf("a=%d b=%d\n", a, b);
printf("a^b=%d\n", c);
return 0;
}
参与异或运算的数满足交换律即a ^ b ^c=a ^ c ^ b
写一个函数返回参数二进制中 1 的个数。
比如: 15 0000 1111 4 个 1
我们先引入一个容易理解的例子,怎么得到一个十进制的数各个位置上的数为多少?
这里我们以一个十进制的三位数 123为例,要想得到它的个位,十位,百位都为多少我们要进行如下操作:
所以我们可以得到第一种方法:
#include
int fun1(int number) {
int count = 0;
while (number!=0)
{
if (number % 2 == 1) {
count++;
}
number = number / 2;
}
return count;
}
int main() {
int num = 0;
int sum = 0;
scanf("%d", &num);
sum=fun1(num);
printf("%d的二进制中有%d个1",num,sum);
return 0;
}
但是这种方法不适用于负数,因此要对形参进行修改为无符号数
#include
int fun1(unsigned int number) {//修改成无符号数,添加unsigned
int count = 0;
while (number!=0)
{
if (number % 2 == 1) {
count++;
}
number = number / 2;
}
return count;
}
int main() {
int num = 0;
int sum = 0;
scanf("%d", &num);
sum=fun1(num);
printf("%d的二进制中有%d个1",num,sum);
return 0;
}
第二种方法:
使用右移操作符,对补码进行移位,再让移位后的数和1进行按位与操作,结果若为1则个数加一
1的32位2进制为00000000000000000000000000000001
具体流程如下:
代码:
#include
int fun1(int number) {
int count = 0;
for (int i = 0; i < 32; i++)
{
//if ((number >> i )% 2 == 1) {//这样算不适用于负数,因为存的是补码,%2的时候用的是原码,多思考一下
// count++;
//}
if (((number >> i) &1) == 1) {
count++;
}
}
return count;
}
int main() {
int num = 0;
int sum = 0;
scanf("%d", &num);
sum = fun1(num);
printf("%d的二进制中有%d个1", num, sum);
return 0;
}
第三种方法:
思路:由最低位向最高位依次将1变为0,这种做法更多的是观察,总结出规律
代码:
#include
int fun1(int number) {
int count = 0;
while (number!=0)
{
number = number & (number - 1);
count++;
}
return count;
}
int main() {
int num = 0;
int sum = 0;
scanf("%d", &num);
sum = fun1(num);
printf("%d的二进制中有%d个1", num, sum);
return 0;
}
#include
int main() {
int arr[10] = { 9,8,7,4,3,5,2,0,1,6 };
printf("%p \n", arr);
printf("%p \n", &arr);
return 0;
}
可以看到结果相同,但是他们的本质却是不同的我们来看
#include
int main() {
int arr[10] = { 9,8,7,4,3,5,2,0,1,6 };
printf("arr =%p \n", arr);
printf("&arr[0] =%p \n", &arr[0]);
printf("&arr =%p \n", &arr);
printf("arr+1 =%p \n", arr + 1);
printf("&arr[0]+1 =%p \n", &arr[0] + 1);
printf("&arr+1 =%p \n", &arr+1);
printf("arr =%d \n", arr);
printf("&arr+1 =%d \n", &arr + 1);
return 0;
}
,由结果我们可以知道&arr+1加的这个1是整个数组所占据的空间40,所以&arr指向的是整个数组的首地址,而数组名arr,&arr[0]指向的是数组元素的首地址
传的是首元素地址,为了提高效率,不可能把数组中所有的元素都传过去
所以在子函数中sizeof(arr)计算出的结果是4/8,而不是整个数组长度
注意:sizeof操作符在使用时有两个例外
sizeof(数组名),这里的数组名是表示整个数组,计算的是整个数组的大小,单位是字节
&数组名,这里的数组名是表示整个数组,&数组名取出的是数组的地址
二维数组arr[0],arr[1]表示什么
#include
int main() {
int arr[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
printf("%p\n", arr[0]);
printf("%p\n", arr[1]);
printf("%p\n", arr[2]);
return 0;
}
二维数组可以看做一维数组的一维数组,由相差正好可以得出16arr[0]就代表arr[0][0], arr[1]就代表arr[1][0],arr[2]就代表arr[2][0]
冒泡排序思想:相邻的两个数据两两比较,然后按顺序排出
代码展示1:
#include
void sort(int arr1[],int sz) {
for (int i = 0; i < sz-1; i++)
{
for (int j = i+1; j < sz; j++)
{
if (arr1[i] > arr1[j]) {
int temp = 0;
temp = arr1[i];
arr1[i] = arr1[j];
arr1[j] = temp;
}
}
}
}
int main() {
int arr[10] = { 9,8,7,4,3,5,2,0,1,6 };
int ArrLength = sizeof(arr) / sizeof(arr[0]);//求数组长度,即包含几个数据
sort(arr,ArrLength);
for (int i = 0; i < ArrLength; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
代码展示2(形参的另一种表示方式):
#include
void sort(int* arr1,int sz) {//这里的参数以指针的形式也可以
for (int i = 0; i < sz-1; i++)
{
for (int j = i+1; j < sz; j++)
{
if (arr1[i] > arr1[j]) {
int temp = 0;
temp = arr1[i];
arr1[i] = arr1[j];
arr1[j] = temp;
}
}
}
}
int main() {
int arr[10] = { 9,8,7,4,3,5,2,0,1,6 };
int ArrLength = sizeof(arr) / sizeof(arr[0]);//求数组长度,即包含几个数据
sort(arr,ArrLength);
for (int i = 0; i < ArrLength; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
错误代码展示:
#include
void sort(int* arr1) {
int ArrLength = sizeof(arr1) / sizeof(arr1[0]);//想在这求求数组长度,即包含几个数据
for (int i = 0; i < ArrLength-1; i++)
{
for (int j = i+1; j < ArrLength; j++)
{
if (arr1[i] > arr1[j]) {
int temp = 0;
temp = arr1[i];
arr1[i] = arr1[j];
arr1[j] = temp;
}
}
}
}
int main() {
int arr[10] = { 9,8,7,4,3,5,2,0,1,6 };
sort(arr);
for (int i = 0; i < 9; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
结果展示:
综上三个代码我们可以提出以下问题?
1.在sort函数中求数组长度为什么不可以结果会出错?但在主函数中就可以
2.函数参数传的是什么?是整个数组还是数组首元素地址
函数中的局部变量如果不加static,则此变量被存放在函数的栈帧中,当调用结束后,函数栈帧被销毁,再一次调用时,此变量值被重新赋值。如果函数中的变量加上static,则此变量不再存放在函数栈帧中,而是被存放在静态区域中,当此函数调用结束后,函数被摧毁,但是此变量没有,因为它不是存放在栈帧中的,当再一次调用时,此变量值为上一次调用结束时的值
#include
int f1(int n)
{
static int i = 1;
if (n >= 5)
return n;
n = n + i;
i++;
return f1(n);
}
int f2(int n)
{
int i = 1;
if (n >= 5)
return n;
n = n + i;
i++;
return f2(n);
}
int main() {
printf("%d\n",f1(1));
printf("%d\n", f2(1));
return 0;
}
首先字符在内存中的大小是1个字节,也就是8个比特位,而整型数据在内存中占4个字节,也就是32个比特位,所以当把整型数据放到字符数据里面是放不下的,此时也就会出现截断现象
例如:
我们举以下例子:
#include
int main() {
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d\n", a);
printf("b=%d\n", b);
printf("c=%d\n", c);
return 0;
}
char为有符号数时,最高位为符号位,当无符号时没有符号位,当有符号位时,整型提升时补char的符号位,当char为无符号数时补0
#include
int main() {
char a = -128;
printf("%u", a);
return 0;
}
结果;
整型提升时看的是它原来的类型,打印时是以输出的格式类型来判断是否有符号位
#include
#include
int main() {
unsigned int i;
for ( i = 9; i >= 0; i--)
{
printf("%u\n", i);
Sleep(1000);
}
return 0;
}
为什么会出现死循环?
因为i的定义为无符号的,所以i始终大于零,但是当i=0时,再减去1不应该是-1吗?按理说应该是-1但是因为,它的定义为无符号数,所以最高位不是符号位而是将-1的二进制位以无符号位的形式打印
2.
#include
int main() {
char a[1000];
int i;
for ( i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
为什么结果为255呢?
首先我们要明确一点,strlen在计算时要找‘\0’,而‘\0’对应的ASCII值为0。
a[i]的值依次为-1,-2,-3…-128,-127…5,4,3,2,1,0,-1,-2,-3…
所以结果为255
当char为有符号的数据时,取值范围为-128~127,当char为无符号数时取值范围为0-255,当理论上超出取值范围时会出现截断现象,导致取值仍为以上取值范围内。
大小端字节序存储:是以字节为单位讨论它在内存中的存储顺序,而不是更小的二进制位
例如:
int main() {
int a = 0x11223344;
return 0;
}
a在内存中的存储16进制为44 33 22 11,两个16进制为一个单位进行存储,而两个十六进制位等于八个比特位,也就是一个字节
0x11223344换算为2进制为等于00010001 00100010 00110011 01000100,
44为低字节内容,11为高字节内容,像上图中低字节内容存放在低地址处的我们称为小端字节序存储,相反把低字节内容存放在高地址处的我们称为大端字节序存储
如何判断机器的大小端?
#include
int check_sys() {
int a = 1;
return *(char*)&a;//对&a强制转换为char类型指针,再进行解引用,char指针解引用时只是取出第一个字节
}
int main() {
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
两种用法:
1.
#include
int main() {
char ch = 'a';
char* p = &ch;
//*p='a'
printf("%c", *p);
return 0;
}
#include
int main() {
char* p = "abcdefg";
printf("%c\n", *p);
printf("%s\n", p);
return 0;
}
结果:
由此可见char类型的p指针指向的是字符串的首元素地址而不是整个字符串
#include
int main() {
char str1[] = "abcdefg";
char str2[] = "abcdefg";
const char* p3 = "abcdefg";
const char* p4 = "abcdefg";
if (str1 == str2) {
printf("str1和str2存放的地址相同\n");
}
else
{
printf("str1和str2存放的地址不相同\n");
}
if (p3 == p4) {
printf("p3和p4存放的地址相同\n");
}
else
{
printf("p3和p4存放的地址不相同\n");
}
return 0;
}
原因:
p3和p4都是指向常量字符串,指向的内容是不可以被修改的,且这里的p3和p4指向的字符串内容相同,所以在内存中不会再开辟新的内存空间来存放该字符串,即只开辟了一块内存空间来存放该字符串,而这两个指针都指向该字符串的首元素地址。但是用相同的常量字符串去初始化数组,就会开辟出不同的内存块,所以str1和str2不同。(主要和数据存储有关,堆区,栈区,静态区,文字常量区,程序代码区等)
指针数组是一个存放指针的数组
int *arr1[10];//整型指针的数组
char *arr2[5];//一级字符指针的数组
char **arr3[4];//二级字符指针的数组
int *arr1[10];含义是定义一个数组arr1[10],数组的类型是int型的指针
arr1的每个元素的类型都是int *
arr1先和[ ]结合后,再和 * 结合。[ ]的优先级高于 *
数组指针是一个指向数组的指针
int (*p)[10]; 含义是定义一个指针变量p,指向int [10]的数组,指针p的类型是int [10]。
一定不要忘记加(),因为[ ]的优先级高于 *
int main() {
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
return 0;
}
由此可以得出arr[0]指的是第一行元素,arr[1]指的是第二行元素,arr[2]指的是第三行元素。
二维数组的第一行就是它的第一个元素,第二行就是它的第二个元素,第三行就是它的第三个元素。
数组名arr和arr[0]都是指首元素地址,在二维数组中首元素地址就是指第一行的地址,第一行的地址也就相当于一个一维数组的地址。
要打印一个二维数组,将二维数组名传过去,如果想用指针来接收应该怎么写呢?
代码:
#include
void printf1(int(*p)[4], int r, int c) {
int i = 0;
for (int i = 0; i < r; i++)
{
int j = 0;
for ( j = 0; j < c; j++)
{
//printf("%d ", (*(p+i))[j]);//p指向第一行,然后再解引用,得到第一行元素,最后再取出这一行的每个元素进行打印
//*(p+i)和p[i]语法上是等价的所以可以写成
printf("%d ", p[i][j]);
}
printf("\n");
}
}
int main() {
int arr[3][4] = { {1,2,3,4},{2,3,4,5},{3,4,5,6} };
printf1(arr, 3, 4);
return 0;
}
因为在二维数组中数组名是指第一行元素的地址,第一行元素也就相当于一个一维数组,所以形参要用数组指针来接收。另外,在一维数组中首元素地址就是第一个元素的地址,所以在对一维数组名作实参时,要用以及指针来接收,而不能用数组指针来接收,而&arr是指整个一维数组,则可以用数组指针来接收。
int *p[10]:指针数组,数组中有10个元素,每个元素的类型都是int *
int (*p)[10]:p是个数组指针,该指针指向一个数组,数组是10个元素,每个元素都是int类型的
int ( *p[10])[5]:p是个数组,数组有10个元素,数组的每个元素类型是:int( * )[5]的数组指针类型
先看p和谁结合,判断p是指针还是数组,去掉和p结合的后,剩下的就是类型
重点说下二维数组传参,用指针如何接收
#include
int main() {
int arr[3][5] = { 0 };
test(arr);
return 0;
}
应该怎么接收arr呢?
我们知道传参传的类型是什么,就应该用什么类型去接收。(如:传了一个指向二维数组第一行元素地址的指针,只用一级指针或二级指针来接收是不可行的,而要用一个数组指针来接收,这个指针也是指向一个数组的)
传的arr是二维数组的第一行所有元素地址,即传的就是个指向一维数组的地址,所以应该用数组指针来接收,而用一级指针,二级指针来接收它都是不正确的,虽然传的是地址,但是它传的是第一行的地址,包含了好几个元素的地址,大小不匹配,自然不可接收成功。
用指针来接收时可以用int (*arr)[5];
#include
int Add(int x,int y) {
return x + y;
}
int main() {
int(*p)(int, int) = &Add;
int ret = (*p)(3, 2);
printf("%d\n", ret);
return 0;
}
对于函数取函数名地址&函数名和函数名意义相同,都是函数的地址。
函数名=&函数名
在通过指向函数的指针对函数进行调用时,指针解引用和不解用都可以调用函数。
所以上述代码这样写和它是完全相同的
#include
int Add(int x,int y) {
return x + y;
}
int main() {
int(*p)(int, int) = Add;
int ret = p(3, 2);
printf("%d\n", ret);
return 0;
}
如果想对指针变量解引用调用函数一定不要忘记加括号。
在排序时,我们通常写的函数只能排一种固定的类型,那有没有一种方法可以用来对所有的数据类型,进行排序呢?库函数中的qsort函数就可以实现这种排序。
首先qsort的函数参数有四个,第一个是数组的起始地址(即数组名),第二个是数组的大小(元素个数),第三个是数组中一个元素的大小(占几个字节),第四个是函数指针(指向所要调用的函数且这个被调用的函数类型和形参与函数指针一致为int (const void* e1, const void* e2))。
先举个例子:
#include
#include
//实现一个比较整型的函数
int compare_int(const void* e1, const void* e2) {//为什么要写成void *呢?
return *(int*)e1 - *(int*)e2;
}
int main() {
int arr[10] = { 1,2,4,3,5,6,7,8,9,10 };
qsort(arr, 10, sizeof(arr[0]), compare_int);
for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
为什么函数形参那要写成void 呢?
因为qsort不知道你要比较什么类型的数据,你传给qsort的第四个实参只是另一个被调函数的地址。void类型的指针优点在于,可以接收任何类型的地址,缺点是你不能对它进行解引用,因为它自己都不知道自己是什么类型的,不知道解引用成什么数据。void指针可以指向任何类型的数据但是它本身的数据类型是void,如果你想使用这个指针就必须先将它强制转换成它指向的那个数据类型指针
对于你自己设计的那个函数,例如上面的int compare_int(const void e1, const void* e2)中的两个函数参数e1,e2,是不需要你来对它进行传参的,qsort内部会自动给它传参。
刚才上面讲到void型指针不能够解引用,那么当它作为参数传给你设计的那个函数时怎么进行比较呢?我们可以进行强制类型转换然后在解引用,因为我们此时已经知道比较的是什么类型的数据了。
再来对qsort函数进行一个总结:如果你要比较int类型的数据那么你就设计一个函数,函数的类型和参数与qsort的第四个参数函数指针指向的函数类型一致为int compare(const void* e1, const void* e2),因为传来的是指向int类型的空指针类型,如果你想使用指针就需要对它进行强转为它指向的数据类型然后解引用后比较,返回值为正数,0,负数,qsort会根据返回值来判断它们的大小。先设计函数为int compare(const void* e1, const void* e2)根据传来的空指针指向的什么类型强转就将它强转为它指向的类型指针然后解引用。
#include
#include
//实现一个比较整型数据的函数
//int compare_int(const void* e1, const void* e2) {
// return *(int*)e1 - *(int*)e2;
//}
//
//void test1() {
// int arr[10] = { 1,2,4,3,5,6,7,8,9,10 };
// qsort(arr, 10, sizeof(arr[0]), compare_int);
// for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
// {
// printf("%d ", arr[i]);
// }
//}
struct Student
{
char name[20];
int age;
};
//int com_stu_age(const void* e1, const void* e2){
// return *(int*)e1 - *(int*)e2;//这是错误的,传过来的e1,e2,是指向结构体的指针,所以要先强制转换成结构体指针在进行解引用
// }
//实现一个比较结构体类型中年龄数据的函数
int com_stu_age(const void* e1, const void* e2) {
return ((struct Student*)e1)->age - ((struct Student*)e2)->age;//e1,e2指向的类型为结构体指针struct Student*
}
void test2() {
struct Student s[3] = { {"zhangsan",20},{"lisi",19},{"wanger",32} };
qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]),com_stu_age);
for (int i = 0; i < 3; i++)
{
printf("%d ", s[i].age);
}
}
int main() {
//test1();
test2();
return 0;
}
#include
#include
#include
struct Student
{
char name[20];
int age;
};
//比较结构体类型中名字的函数
int com_stu_name(const void* e1, const void* e2) {
//在对char类型数据进行比较时,要使用strcmp函数,同时包含头文件#include
return strcmp(((struct Student*)e1)->name, ((struct Student*)e2)->name);//e1,e2指向的类型为结构体指针struct Student*
}
void test2() {
struct Student s[3] = { {"zhangsan",20},{"lisi",19},{"wanger",32} };
qsort(s, sizeof(s) / sizeof(s[0]), sizeof(s[0]), com_stu_name);
for (int i = 0; i < 3; i++)
{
printf("%s ", s[i].name);
}
}
int main() {
//test1();
test2();
return 0;
}
strlen函数是用来计算字符串长度的,不包括‘\0’
使用:
#include
#include
int main() {
char arr[] = "abcdef";
int len = strlen(arr);
printf("%d\n", len);
return 0;
}
模拟实现:
先看一下库函数strlen是怎么定义的
可知库函数中strlen的返回值为无符号的int类型,形参为不可修改的指针变量,返回类型也可写成int,但字符串长度不可能为负数所以为了保险起见要写成size_t;两个size_t类型的值相减值也是size_t;
写法一:
#include
#include
int my_strlen(const char* str)
{
assert(str != NULL);//预防字符串为空
int count = 0;
while (*str++ != '\0')
{
count++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int ret = my_strlen(arr);
printf("%d\n", ret);
return 0;
}
写法二:递归的方式
#include
#include
int my_strlen(const char* str)
{
assert(str != NULL);//预防字符串为空
if (*str != '\0')
{
return 1 + my_strlen(str+1);
}
else
{
return 0;
}
}
int main()
{
char arr[] = "abcdef";
int ret = my_strlen(arr);
printf("%d\n", ret);
return 0;
}
写法三:指针减指针
#include
#include
int my_strlen(const char* str)
{
assert(str != NULL);//预防字符串为空
const char* start = str;
/*while (*str++ != '\0') //这种写法不对
{
;
}*/
while (*str != '\0')
{
str++;
}
return str - start;//两个指针相减恰好为两指针之间的元素个数
}
int main()
{
char arr[] = "abcdef";
int ret = my_strlen(arr);
printf("%d\n", ret);
return 0;
}
重点:including the terminating null character (and stopping at that point).意为拷贝的值包括停止字符
传参时第一个参数为要拷贝参数,第二个参数为被拷贝参数
#include
#include
int main() {
char str1[20] = "abcdefg";
char str2[] = "zxcv";
strcpy(str1, str2);
printf("%s\n", str1);
return 0;
}
结果:
拷贝之后:
可知在拷贝时‘\0’也被拷贝过来了。
2.
//提前结束被拷贝的字符串
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main() {
char str1[20] = "abcdefg";
char str2[] = "zxcvv\0bnm";
strcpy(str1, str2);
printf("%s\n", str1);
return 0;
}
1.目标空间必须足够大且可变
2.结束字符也会被拷贝过去
3源字符串必须以‘\0’结束
错误示例:
#include
#include
int main() {
char* p = "abcdefg";
char str2[] = "zxcv";
strcpy(p, str2);
printf("%s\n", *p);
return 0;
}
目标字符串常量字符串不允许被修改,必须写成数组的形式
#include
#include
char* my_strcpy(char* dest,const char* src)
{
//函数返回值为目标空间的起始地址,*dest++ = *src++会改变dest的地址,所以要先保存
char* ret = dest;
assert(src && dest);
while (*dest++ = *src++) //加到最后先把结束字符拷贝过去,然后在判定循环是否可以再运行
{
;
}
return ret;
}
int main() {
char str1[20] = "xxxx";
char str2[] = "abcdefghi";
my_strcpy(str1, str2);
printf("%s\n", str1);
return 0;
}
所传参数为地址,所以两个参数都要进行判空操作,都不能为空指针
#include
#include
int main()
{
char str1[20] = "abcdef";
char str2[] = "zxcvbn";
strcat(str1, str2);
printf("%s\n", str1);
return 0;
}
#include
#include
char* my_strcat(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest;
while (*dest)
{
dest++;
}
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char str1[20] = "abcdef ";
char str2[] = "zxcvbn";
my_strcat(str1, str2);
printf("%s\n", str1);
return 0;
}
对齐规则:
1.结构体的第一个成员,对齐到结构体在内存中存放位置的0偏移处
2.从第二个成员开始,每个成员都要对齐到(一个对齐数)的整数倍处对齐数。
对齐数:结构体成员自身大小和默认对齐数的较小值
VS:默认对齐数为8
Linux gcc:没有默认对齐数,对齐数就是结构体成员的自身大小
3.结构体的总大小,必须是所有成员的对齐数中最大对齐数的整数倍
4.如果结构体中嵌套了结构体成员,要将嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍。
结构体的总大小必须是最大对齐数的整数倍,这里的最大对齐数是:包含嵌套结构体成员中的对齐数的所有对齐数中的最大值。
举例:
#include
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
return 0;
}
结果:
结果一个为12 一个为8,为什么同样的子成员,结构体所占空间大小不同?又为什么结构体所占内存大小不是各个子成员类型所占内存大小之和?
#include
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S3));
printf("%d\n", sizeof(struct S4));
return 0;
}
为了更好地利用内存空间,比如在结构体中定义了一个int型数据,那么在使用这个结构体变量时,此变量就会向内存申请一个int型的空间4/8个字节,但如果这个int类型的数据只需要两个比特位就能够存储,那么内存空间就浪费了30个比特位,而如果结构体中还有一个int型的数据8个比特位就存的下,那它也会浪费掉一部分内存空间,而位段的作用就是减少这种内存空间的浪费。
位段的成员必须是int,unsigned int,signed以及char类型数据。
声明:位段的成员后加一个冒号和一个数字。
struct Stu
{
int _a : 2;
int _b : 3;
char _c : 3;
};
位段的存储方式是从左向右还是从右向左是未定义的,当这个位段成员的剩余内存空间不足以容纳下一个位段成员时,是舍弃还是继续利用剩下的和加上新开辟的来容纳他,都是未定义的。
参数为两个字符串的地址,在str1中找str2不包括结束符。
函数使用
#include
#include
int main()
{
char* str1 = "abcdeffgmnlo";
char* str2 = "de";
char str3[] = "fg";
char* pc1 = strstr(str1, str2);
printf("%s\n", pc1);
char* pc2 = strstr(str1, str3);
printf("%s\n", pc2);
char* pc3 = strstr(str1, "mmm");
printf("%s\n", pc3);
return 0;
}
函数返回值为在主串中找到的子串首元素地址
打印时以这个地址开始打印找到结束字符停止打印
#include
#include
const char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
if (*str2 == '\0')
{
return str1;
}
const char* cp1 = NULL;
const char* cp2 = NULL;
const char* pmove = str1;
while (pmove != '\0')
{
cp1 = pmove;
cp2 = str2;
while (*cp1 == *cp2 && *cp1 != '\0' && *cp2 != '\0')
{
cp1++;
cp2++;
}
if (*cp2 == '\0')
{
return pmove;
}
pmove++;
}
return NULL;
}
int main()
{
char* str1 = "abcddefg";
char* str2 = "de";
char* ret = my_strstr(str1, str2);
if (ret == NULL)
{
printf("找不到");
}
else
{
printf("%s\n", ret);
}
return 0;
}
这个函数的模拟实现可以很好的锻炼你的思维,多想多上手。
#include
#include
int main()
{
char arr[] = "[email protected]";
char* p = "@.";
char buf[20] = { 0 };
strcpy(buf, arr);
char* ret = strtok(buf, p);
printf("%s\n", ret);
ret = strtok(NULL, p);
printf("%s\n", ret);
ret = strtok(NULL, p);
printf("%s\n", ret);
return 0;
}
这里的@和.为分隔符,在调用strtok函数时会把这两个分隔符转换为‘\0’,并返回分隔符之前的字符串首地址,在第二次调用时要传空指针过去,才能返回第二个字符串的首地址,这里为什么要把arr赋给buf这个新数组?因为strtok调用后会改变原字符串。如果字符串中不存在更多的标记,则返回 NULL 指针。
代码改进:
#include
#include
int main()
{
char arr[] = "[email protected]";
char* p = "@.";
char buf[20] = { 0 };
strcpy(buf, arr);
char* ret = NULL;
for (ret = strtok(buf, p); ret != NULL;ret = strtok(NULL,p))
{
printf("%s\n", ret);
}
return 0;
}
当一个函数运行错误时,它会将对应的错误码存放在一个int类型的全局变量errno中,strerror函数是将错误码转化成错误信息的函数,参数是一个int类型的errno错误码,返回值为错误信息的首地址。
#include
#include
#include
int main()
{
FILE* pf = fopen("test.txt", "r");//打开失败返回NULL
if (pf == NULL)
{
printf("%s", strerror(errno));
}
else
{
fclose(pf);
pf = NULL;
}
return 0;
}
注意:errno也要包含头文件
perror函数没有返回值,与strerror函数不同,strerror返回错误信息的首地址,要想知道错误信息还需要打印,perror直接打印没有返回值。
打印的信息为冒号+空格+错误信息
#include
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("test.txt");
}
else
{
fclose(pf);
pf = NULL;
}
return 0;
}
返回值为time_t
//生成时间戳
#include
#include
int main()
{
int ret = (unsigned int)time(NULL);
printf("%d\n", ret);
return 0;
}
#include
#include
#include
int main()
{
srand((unsigned int)time(NULL));
int ret = rand();
printf("%d\n", ret);
return 0;
}
srand函数参数为unsigned int所以要对time函数返回值time_t进行强转
在调用rand函数生成随机数前要先调用srand函数获得一个变化的值去初始化rand函数随机数的生成起点,所以rand函数和srand函数要一起使用
RAND_MAX的值为32767。
如果不设置随机数的生成起点,它的随机值不发生变化。所以要先设置随机数生成起点,即先设置一个变化的值,而计算机中时间戳是在一直变化的,所以把time函数返回值传给它。
之前的拷贝或赋值等都是对字符串操作的,而对内存中其它数据如结构体,数组中的数据的拷贝,都是要用内存函数来完成的。
第一个参数为目标地址,第二个参数是源地址,第三个参数是要拷贝的字节大小。
这个函数拷贝相对于strcpy函数来说不仅限于对字符串的拷贝。
#include
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9 };
int arr2[10] = { 0 };
memcpy(arr2, arr1,20);
}
#include
void my_memcpy(void* dest, const void* src, size_t num)//为什么参数类型是void?
{
while (num--)
{
*(char*)dest = *(char*)src;//为什么要强转为char指针类型?
dest = (char*)dest + 1;//为什么(char*)dest++是错误的?
src = (char*)src + 1;
}
}
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9 };
int arr2[10] = { 0 };
my_memcpy(arr2, arr1, 20);
return 0;
}
以上模拟的memcpy当在拷贝自身数值时可能会出错
拷贝自身时有三种情况一种是源地址在目标地址的后面,一种是源地址和目标地址位置相同,一种是源地址在目标地址的前面
我们可以将它分为两种情况一种是源地址在目标地址前面时我们按照从前向后拷贝,一种是源地址在目标地址后面或相同时我们按照从后向前拷贝,为的是避免在拷贝过程中有的数据被覆盖掉。
#include
void my_memcpy(void* dest, const void* src, size_t num)//为什么参数类型是void?
{
void* ret = dest;
if (src >= dest)
{
//从前向后拷贝
while (num--)
{
*(char*)dest = *(char*)src;//为什么要强转为char指针类型?
dest = (char*)dest + 1;//为什么(char*)dest++是错误的?
src = (char*)src + 1;
}
}
else
{
//从后向前拷贝
while (num--)
{
/**(char*)dest + num = *(char*)src + num;*///这样写是错误的
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
不同编译器拷贝的结果不同,所以对自身进行拷贝时尽量使用memmove函数来进行拷贝
第一个参数为目标地址,第二个参数是源地址,第三个参数是要拷贝的字节大小。
//memmove可以实现重叠内存的拷贝
#include
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[10] = { 1,2,3,4,5,6,7,8,9,10 };
memmove(arr1 + 2, arr1, 20);
memmove(arr2, arr2 + 2, 20);
return 0;
}
参数是要开辟内存空间的大小
开辟成功则返回值为开辟空间的首地址,若开辟失败则返回一个空指针NULL
第一个参数为开辟空间的元素个数,第二个参数为每个元素的大小
开辟成功返回值为开辟空间的首地址,开辟失败返回值为空指针NULL
除了参数不同外开辟后的结果也是不同的,malloc开辟好的空间不会被赋值,calloc开辟好的空间会被赋值
#include
#include
#include
int main()
{
int* ptr1 = (int*)malloc(10 * sizeof(int));
assert(ptr1);
int* ptr2 = (int*)calloc(10, 4);
assert(ptr2);
free(ptr1);
free(ptr2);
ptr1 = NULL;
ptr2 = NULL;
return 0;
}
参数为一个指针,指向的是用malloc,calloc,realloc函数开辟的空间的,如果不是动态开辟空间,结果不确定,标准没有定义。
第一个参数为已经动态申请好的空间地址,第二个参数为重新开辟空间的大小,不是再增加这么大的空间大小,而是总共开辟这么大的空间大小。
#include
#include
#include
int main()
{
int* ptr1 = (int*)malloc(20);
assert(ptr1);
int* ptr2 = (int*)realloc(ptr1,40);
assert(ptr2);
free(ptr2);
ptr2 = NULL;
return 0;
}
**注意:**使用realloc函数时,要用另一个指针来接收,如ptr2。因为有可能在这个地址后的内存空间大小不能满足需要扩容的空间大小,所以有可能重新开辟的空间起始地址与第一次开辟的起始地址不一致。
如果扩容失败则返回空指针NULL
#include
#include
int main()
{
printf("1\n");
Sleep(1000);
printf("2\n");
Sleep(1000);
system("cls");
return 0;
}
Sleep函数生成时间间隔,单位是毫秒。
system(“cls”);用来清除屏幕。
#include
#include
struct S
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S, c1));
printf("%d\n", offsetof(struct S, i));
printf("%d\n", offsetof(struct S, c2));
return 0;
}
#include
#define OFFSETOF(type,member) ((size_t)&(((type*)0)->member))
struct S
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", OFFSETOF(struct S, c1));
printf("%d\n", OFFSETOF(struct S, i));
printf("%d\n", OFFSETOF(struct S, c2));
return 0;
}
结果:
数值类型强制类型转换成指针
类型转换可以由数值转换成指针类型
转换的结果是
可以得出转换后的地址计算得出就是对应的数值,20对应的十六进制就是0x00000014
同理再将地址强制类型转换成数值时,由地址对应的十六进制计算得出,该数值大小就是地址所对应的。
#include
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
//printf("%s\n", __STDC__);
return 0;
}
其中__STDC__在该环境下未定义
在用#define定义宏时,千万不要吝啬对括号的使用,因为很有可能会导致错误
例如
#include
#define SQUARE(x) (x)*(x)
int main()
{
printf("%d\n", SQUARE(5 + 1));
return 0;
}
如果定义宏时写成 #define SQUARE(x) x*x,则结果就会为11,因为宏是直接替换
又如:
#include
#define DOUBLE(x) (x+x)
int main()
{
printf("%d\n", 2*DOUBLE(1+2));
return 0;
}
结果为12,如果不在外部加上括号结果就会为7,所以在定义宏时一定要在外部加上括号,从而得到预期的效果减少错误。
注意
宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。即不能在定义时自己里面出现自己。
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
首先要明白一个问题
#include
int main()
{
printf("hello world\n");
printf("hello ""world\n");
printf("hello","world\n");
return 0;
}
printf的参数只有一个,并且当中间没有逗号时,两个字符串会自动合为一个字符串。
利用#把参数插入到字符串中
#include
int main()
{
int a = 10;
printf("The value of a is %d\n", a);
int b = 20;
printf("The value of b is %d\n", b);
return 0;
}
会发现上述语句有重复操作,通过定义宏来简化
如果这样写
#include
#define PRINTF(x) printf("The value of x is %d\n",x)
int main()
{
int a = 10;
PRINTF(a);
return 0;
}
变量名被固定为x,怎么让字符串中的x也能在预处理中被发现且改变呢?
#include
#define PRINTF(x) printf("The value of " #x " is %d\n",x)
int main()
{
int a = 10;
PRINTF(a);
return 0;
}
#的作用就是把字符串中的参数直接插入到字符串中,预处理完成之后PRINTF(a);语句就变为
printf(“The value of " “a” " is %d\n”,a); #把一个宏参数转换成它所对应的字符串,然后多个字符串再合并进行打印
#include
#define PRINTF(format,x) printf("The value of " #x " is "format"\n",x)
int main()
{
float f = 3.14f;
PRINTF("%f", f);
return 0;
}
因为format不是字符串所以可以被检索到,然后被替换成“%f”,注意传过去的参数有双引号
另一种写法在非字符串参数format前加#
#include
#define PRINTF(format,x) printf("The value of " #x " is "#format"\n",x)
int main()
{
float f = 3.14f;
PRINTF(%f, f);
return 0;
}
#include
#define PRINTF(format,x) printf("The value of " #x " is "format"\n",x)
int main()
{
float f = 3.14f;
PRINTF(%f, f);
return 0;
}
##的作用:
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
#include
#define CAT(x,y) x##y
int main()
{
int Data210 = 20230210;
printf("%d\n", CAT(Data, 210));
return 0;
}
注意这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
这条指令用于移除一个宏定义
#include
#define S 5
#undef S
int main()
{
printf("%d\n", S);
return 0;
}
endif英文翻译:结束条件编译
满足条件编译不满足条件不编译
未定义不满足条件
定义了要执行的语句颜色已经发生了变化
还有多种嵌套条件编译指令在这里就不过多的讲述了
如果在同一个程序中,一个文件被包含了多次就会降低程序运行的效率,我们可以先判断该文件是否已经被包含
为了避免头文件的重复引入我们可以用以下两种方法
每个头文件的开头写:
__TEST__H__是自己起的名字,如果在第一次包含这个头文件时这个自己定义的宏__TEST__H__是没有被定义的则定义,当第二次包含这个头文件时它就已经被定义过了,则不再重复定义
或者#pragma once
思考总结:
头文件中的 ifndef/define/endif是干什么用的?
#include
pow的翻译是指数表达式
第一个参数为底数,第二个参数为指数
返回值为:
头文件为include
#include
#include
int main()
{
int ret = (int)pow(10, 2);
printf("%d\n", ret);
return 0;
}
#include
int main()
{
int a = 10;
a = ((a & 0x55555555) << 1) + ((a & 0xaaaaaaaa) >> 1);
printf("%d\n", a);
return 0;
}
#include
#define SWAP(x) (x=((x & 0x55555555) << 1) + ((x & 0xaaaaaaaa) >> 1))
int main()
{
int a = 10;
SWAP(a);
printf("%d\n", a);
return 0;
}
思想:得到偶数位就是先按位与,0x55555555对应的二进制位就是
01010101010101010101010101010101按位与的结果是只剩下偶数位奇数位全为0,再向左移动一位移到奇数位,得到奇数位就是按位与上0xaaaaaaaa对应的二进制位就是10101010101010101010101010101010
按位与的结果就是只剩下奇数位偶数位全为0,再向右偏移一位移到偶数位上,此时向左向右移的时候都是补0,所以不会影响(可以去看位操作符相关内容)
又因为一个数等于所有二进制位上对应的值相加,同理也就等于所有奇数位对应的值加上偶数位对应的值所以两数相加,所以便可得到奇偶数位互换的结果
#include
int main()
{
char c1 = getchar();
char c2 = getchar();
char c3 = getchar();
putchar(c1);
putchar(c2);
putchar(c3);
return 0;
}
注意:字符输入时是连续输入并且连续读取,当输入第一个数据后不需要加空格(即结束分隔符),它把空格也会作为一个字符输入
加了空格后的结果为:
#include
int main()
{
//需要给定数组大小,如果只给了一个值0,则默认数组大小为1,再用gets对它赋值时
//多余1个值则会造成越界访问
char str[20];
gets(str);
puts(str);
return 0;
}
gets函数对于空格也会把它作为一个字符进行输入只有当遇到换行符时才会停止输入,并且在输入后该字符串会自动在末尾添加一个换行符
scanf则不会自动添加,并且scanf输入的每个元素类型是可以控制的,有多种类型,而gets函数输入的元素都为char类型。