C语言强化 day03 指针强化与位运算
1. 指针强化
1.1 内存分配(calloc与realloc函数)
对于堆区内存操作的函数,在基础阶段我们学过了malloc
与free
。在此继续深入学习calloc
函数与realloc
函数,这两个函数也是进行内存分配的。函数原型以及参数如下所示:
函数名 | 原型 | 返回值 | 参数 | 作用 | tip |
---|---|---|---|---|---|
calloc | void *calloc(size_t nmemb, size_t size); | 成功返回分配空间的起始地址,失败则返回NULL | nmemb: 所需内存单元数量 size: 每个内存单元的大小(单位:字节) | 在内存动态存储区(堆区)分配nmemb块长度为size字节的连续区域。并且将分配的内存置0 | malloc 函数不置0,calloc 函数置0 |
realloc | void *realloc(void *ptr, size_t size); | 成功返回新分配的堆内存地址,失败则返回NULL | ptr: 为之前用malloc 或者calloc 分配的内存空间地址,如果此地址为NULL ,则该函数和malloc 功能一致 size: 重新分配内存的大小(单位:字节) | 重新分配用malloc 或者calloc 函数在堆中分配的内存大小 | realloc 函数不会对新分配的内存空间置0。如果指定的地址又连续的内存空间,realloc 函数会在已有的地址基础上加内存;如果指定的地址后面没有空间,则realloc 函数会重新分配连续内存,并把旧内存的值拷贝到新的空间中,同时释放旧内存 |
接下来看一段关于calloc
和realloc
函数的示例代码:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
// calloc
void test01()
{
int *p = malloc(sizeof(int) * 5);
for (int i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
putchar('\n');
if (p != NULL)
{
free(p);
p = NULL;
}
// calloc分配在堆区,与malloc不一样的是calloc分配会初始化数据为0
p = calloc(5, sizeof(int));
for (int i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
putchar('\n');
if (p != NULL)
{
free(p);
p = NULL;
}
}
// realloc 重新分配内存
void test02()
{
int *p = malloc(sizeof(int)* 5);
printf("p = %p\n", p);
for (int i = 0; i < 5; i++)
{
p[i] = i * 2;
printf("%d ", p[i]);
}
putchar('\n');
// 重新分配内存比原来大,不会初始化新空间为0
p = realloc(p, sizeof(int) * 6);
printf("p = %p\n", p);
for (int i = 0; i < 6; i++)
{
printf("%d ", p[i]);
}
putchar('\n');
p = realloc(p, sizeof(int)* 1000);
printf("p = %p\n", p);
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
putchar('\n');
// 如果重新分配的内存比原来小,那么释放后续空间,只有权限操作申请的空间
p = realloc(p, sizeof(int)* 2);
printf("p = %p\n", p);
for (int i = 0; i < 2; i++)
{
printf("%d ", p[i]);
}
putchar('\n');
printf("p[3] = %d\n", p[3]);
if (p != NULL)
{
free(p);
p = NULL;
}
}
int main(int argc, char* argv[])
{
//test01();
test02();
system("pause");
return 0;
}
#endif
1.2 sscanf的使用
在基础阶段我们也接触过了sscanf
函数的简单用法,而在强化阶段我们主要是使用sscanf
函数与正则表达式配合起来进行使用。关于sscanf
函数的原型以及参数在此就不做更多的介绍。
在实际操作中,我们或许会遇到将一个字符串中的某个子串单独拿出来的操作。比如把一个时间字符串2023/09/23 09:20:59
中的年月日时分秒单独拿出来进行运算,或者是一个字符串中code@qq.com
中将邮箱名称或者邮箱的域名单独拿出来的情况。这些情况都需要使用到正则表达式,正则表达式的主要规则如下:
格式 | 作用 |
---|---|
%*s 或者 %*d | 跳过数据 |
%[width]s | 读指定宽度的数据 |
%[a-z] | 匹配a到z中任意字符(尽可能多的匹配) |
%[aBc] | 匹配a、B、c中的一员,贪婪 |
%[^a] | 匹配非a的任意字符,贪婪 |
%[^a-z] | 读取除a-z以外的所有字符 |
接下来看看关于正则表达式与sscanf
函数结合使用的示例代码:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
// %*s 或者 %*d 跳过数据
void test01()
{
char *str = "520jiutianxuannv";
char buf[1024] = { 0 };
sscanf(str, "%*d%s", buf);
printf("%s\n", buf);
}
void test02()
{
char *str1 = "jiutianxuannv520";
char *str2 = "jiutianxuannv 520";
char *str3 = "jiutianxuannv\t520";
char buf1[1024] = { 0 };
char buf2[1024] = { 0 };
char buf3[1024] = { 0 };
sscanf(str1, "%*[a-z]%s", buf1);
printf("%s\n", buf1);
// 忽略遇到空格或者\t代表忽略结束
sscanf(str2, "%*s%s", buf2);
printf("%s\n", buf2);
sscanf(str3, "%*s%s", buf3);
printf("%s\n", buf3);
}
// %[width]s 读取指定宽度的数据
void test03()
{
char *str = "JiuTianXuanNv666";
char buf[1024] = { 0 };
sscanf(str, "%13s", buf);
printf("%s\n", buf);
}
// %[a-z] 匹配a到z中的任意字符(尽可能多的匹配)
void test04()
{
char *str = "520jiutianxuannv@iloveu";
char buf[1024] = { 0 };
sscanf(str, "%*d%[a-z]", buf);
printf("%s\n", buf);
}
void test05()
{
char *str = "5201314jiutianxuannv";
char buf[1024] = { 0 };
sscanf(str, "%[0-9]", buf);
printf("%s\n", buf);
}
// %[aBc] 匹配a、B、c中的一员
void test06()
{
char *str = "jiutianxuannv";
char buf[1024] = { 0 };
sscanf(str, "%[aijntu]", buf);
printf("%s\n", buf);
}
// %[^a] 匹配非a的任意字符
void test07()
{
char *str = "jiutianxuannv";
char buf[1024] = { 0 };
sscanf(str, "%[^x]", buf);
printf("%s\n", buf);
}
// %[^a-z] 表示读取除a-z以外的所有字符
void test08()
{
char *str = "jiutianxuannv520";
char buf[1024] = { 0 };
sscanf(str, "%[^0-9]", buf);
printf("%s\n", buf);
}
// 练习1: 匹配IP地址中的所有数字
void test09()
{
char *ip = "127.0.0.1";
int ip1 = 0;
int ip2 = 0;
int ip3 = 0;
int ip4 = 0;
sscanf(ip, "%d.%d.%d.%d", &ip1, &ip2, &ip3, &ip4);
printf("%d %d %d %d\n", ip1, ip2, ip3, ip4);
}
// 匹配#与@之间的名字
void test10()
{
char *str = "abcdef#zhangtao@123456";
char buf[1024] = { 0 };
sscanf(str, "%*[^#]#%[^@]", buf);
printf("%s\n", buf);
}
// 已给定字符串为: helloworld@itcast.cn,请编码实现 helloworld 输出和 itcast.cn 输出。
void test11()
{
char *str = "helloworld@itcast.cn";
char buf1[1024] = { 0 };
char buf2[1024] = { 0 };
sscanf(str, "%[^@]%*[@]%s", buf1, buf2);
printf("%s %s\n", buf1, buf2);
}
int main(int argc, char* argv[])
{
//test01();
//test02();
//test03();
//test04();
//test05();
//test06();
//test07();
//test08();
//test09();
//test10();
test11();
system("pause");
return 0;
}
#endif
1.3 字符串操作(查找子串)
接下来看一个关于字符串查找的操作,在一个字符串中查找某一个子串的位置。在此操作中,我们需要将子串一直向右移动对原串进行匹配操作。由于在此之前未提及算法,这里的匹配操作以BF算法为例,也就是暴力匹配,而非KMP算法。
在子串的对比过程中,我们可以使用指针一个一个字符与原串进行对比,也可以使用memcmp
函数将一大片的内存空间进行对比。下面展示这两种方法的示例代码:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
int myStrstr(char *str, char *substr)
{
int num = 0;
while (*str != '\0')
{
if (*str != *substr)
{
str++;
num++;
continue;
}
// 创建两个临时指针做二次对比
char *tmpStr = str;
char *tmpSubstr = substr;
while (*tmpSubstr != '\0')
{
if (*tmpStr != *tmpSubstr)
{
// 匹配失败
str++;
num++;
break;
}
tmpStr++;
tmpSubstr++;
}
if (*tmpSubstr == '\0')
{
// 匹配成功
return num;
}
}
return -1;
}
int myStrstrPro(char *str, char *substr)
{
int num = 0;
unsigned int strLen = strlen(str);
unsigned int substrLen = strlen(substr);
while ( strLen - num >= substrLen)
{
if (memcmp(str + num, substr, substrLen) != 0)
{
num++;
}
else
{
return num;
}
}
return -1;
}
int main(int argc, char* argv[])
{
char *str = "jiutianxuannv!love you!";
int num = myStrstr(str, "xuannv");
if (num != -1)
{
printf("查找到了子串,位置为: %d\n", num);
}
else
{
printf("未查到子串\n");
}
num = myStrstrPro(str, "love");
if (num != -1)
{
printf("查找到了子串,位置为: %d\n", num);
}
else
{
printf("未查到子串\n");
}
system("pause");
return 0;
}
#endif
1.4 指针易错点
在一级指针中,主要有以下四个易错点:
- 指针越界:常常发生在字符数组中用
%s
输出的时候字符数组中无字符串结束标记'\0'
。 - 指针叠加会不断改变指针指向方向:比如在堆区申请一块内存的时候,后续对堆内存的操作直接将存储堆区地址的指针往后移动,从而使指针指向的位置发生了改变,此时释放堆区内存就会出错。
- 返回局部变量地址:比如在被调函数中定义了一个字符数组,而返回值是这个字符数组的首地址。而主调函数拿到这个字符数组的地址时被调函数栈帧已经被释放,此时拿到的这个地址是个没有权限访问的空间,就算能操作该空间的数据也不受保护。
- 同一块内存释放多次:在堆区申请的内存空间释放之后,该指针指向的空间也就是为无权限操作的空间了,也就是野指针,此时不能对该指针再一次进行释放的操作,此时程序会报错。
接下来我们看一段关于易错代码的示例:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
int main(int argc, char* argv[])
{
char *p = malloc(sizeof(char)* 64);
char *pp = p; // 通过创建临时指针操作内存,防止出错
for (int i = 0; i < 10; i++)
{
*pp = i + 97;
printf("%c ", *pp);
//p++; // 更改指针位置释放出错
pp++;
}
putchar('\n');
pp = NULL;
if (p != NULL)
{
free(p);
p = NULL;
}
system("pause");
return 0;
}
#endif
1.5 const 的使用场景
在C语言中,const
是用于定义一个只读变量。当在全局区使用const
定义一个变量的时候,该变量会放在常量区;而定义一个局部变量的时候,该变量会放在栈区。但是相同的时候定义了之后都不能去直接修改该变量的值。
const
使用最多的场景在于修饰函数的形式参数。此时可以防止后续对该参数的误操作。比如我们定义了一个show
函数对一个学生的结构体的基础信息进行了打印,而在这个函数操作结构体的时候,就不应该修改该学生的信息,此时就可以通过对函数的形式参数使用const
修饰起到防止误操作的目的。接下来看一段关于示例代码:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
struct Person
{
char name[64];
unsigned int age;
unsigned int id;
double score;
};
// 将struct Person p 改为 struct Person *p 节省资源
// const 使用修饰形参,防止误操作
void show(const struct Person *p)
{
//p->age = 20;
printf("name: %s, age: %u, id: %u, score:%.2lf\n", p->name, p->age, p->id, p->score);
}
int main(int argc, char* argv[])
{
struct Person p = {"迪杰斯特拉", 50, 1, 520.1314};
show(&p);
system("pause");
return 0;
}
#endif
1.6 二级指针做形参的输入特性
二级指针做函数的输入特性是指由主调函数分配内存。示例代码如下:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
void printArray(int **pArray, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", *pArray[i]);
}
putchar('\n');
}
void test01()
{
// 创建在堆区
int **pArray = malloc(sizeof(int) * 5);
// 在栈上创建5个数据
int a1 = 10;
int a2 = 20;
int a3 = 30;
int a4 = 40;
int a5 = 50;
pArray[0] = &a1;
pArray[1] = &a2;
pArray[2] = &a3;
pArray[3] = &a4;
pArray[4] = &a5;
// 打印数组
printArray(pArray, 5);
// 释放堆区数据
if (pArray != NULL)
{
free(pArray);
pArray = NULL;
}
}
void freeSpace(int **pArray, int len)
{
for (int i = 0; i < len; i++)
{
free(pArray[i]);
pArray[i] = NULL;
}
}
void test02()
{
// 创建在栈区
int *pArray[5];
for (int i = 0; i < 5; i++)
{
pArray[i] = malloc(sizeof(int));
*(pArray[i]) = i + 5;
}
printArray(pArray, 5);
// 释放堆区
freeSpace(pArray, 5);
}
int main(int argc, char* argv[])
{
//test01();
test02();
system("pause");
return 0;
}
#endif
1.7 二级指针做形参的输出特性
二级指针做函数参数的输出特性是指由被调函数函数分配内存。相关示例代码如下:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
void allocateSpace(int **p)
{
int *tmp = malloc(sizeof(int)* 10);
for (int i = 0; i < 10; i++)
{
tmp[i] = 100 + i;
}
*p = tmp;
}
void printArray(int **pArray, int len)
{
for (int i = 0; i < len; i++)
{
printf("%d ", (*pArray)[i]);
}
putchar('\n');
}
void freeSpace(int **p)
{
if (*p != NULL)
{
free(*p);
*p = NULL;
}
}
int main(int argc, char* argv[])
{
int *p = NULL;
allocateSpace(&p);
printArray(&p, 10);
freeSpace(&p);
if (p == NULL)
{
printf("空指针\n");
}
else
{
printf("野指针\n");
}
system("pause");
return 0;
}
#endif
1.8 二级指针文件读写示例
在这里我们需要将一个文件里面的每一行数据进行读取出来并将数据存入到堆区中。对应的操作为
1) 打开文件
2) 统计文件的行数
3) 在堆区分配相应的行内存(二级指针)
4) 对每一行的数据存到指定行中(一级指针--字符串指针)
5) 输出存储的所有内容
6) 关闭文件
7) 将堆区开辟的所有内存释放(使用三级指针)
在编写代码之前,在对应的目录下创建一个文本文件,此处创建的为aaa.txt
文件,在里面写入想要写入的内容。
使用二级指针文件读写的代码如下:
#if 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
// 获取有效行数
int getFileLines(FILE *fp)
{
if (fp == NULL)
{
return -1;
}
char buf[1024];
int lines = 0;
while (fgets(buf, 1024, fp))
{
lines++;
//printf("%s", buf);
}
// 将文件光标置首
fseek(fp, 0, SEEK_SET);
return lines;
}
// 读取数据放入到pArray中
void readFileData(FILE *fp, int len, char **pArray)
{
if (fp == NULL)
{
return;
}
if (len <= 0)
{
return;
}
if (pArray == NULL)
{
return;
}
char buf[1024] = { 0 };
for (int i = 0; i < len; i++)
{
fgets(buf, 1024, fp);
pArray[i] = malloc(sizeof(char) * (strlen(buf) + 1));
strcpy(pArray[i], buf);
memset(buf, 0, sizeof buf);
}
}
void showFileData(char **pArray, int len)
{
if (pArray == NULL)
{
return;
}
if (len <= 0)
{
return;
}
for (int i = 0; i < len; i++)
{
printf("%03d行: %s", i + 1, pArray[i]);
}
}
// 释放堆上分配的内存
void freeSpace(char ***p, int len)
{
for (int i = 0; i < len; i++)
{
free((*p)[i]);
(*p)[i] = NULL;
}
free(*p);
*p = NULL;
}
int main(int argc, char* argv[])
{
char *filename = "aaa.txt";
FILE *fp = fopen(filename, "r");
if (fp == NULL)
{
perror("fopen error: ");
return;
}
// 统计有效行数
int len = getFileLines(fp);
printf("文件有效行数: %d\n", len);
char **pArray = malloc(sizeof(char *) * len);
// 读取文件中的数据并且放入到pArray中
readFileData(fp, len, pArray);
// 输出数据
showFileData(pArray, len);
// 关闭文件
fclose(fp);
freeSpace(&pArray, len);
system("pause");
return 0;
}
#endif
2. 位运算
位运算是对二进制进行操作的一种运算。在计算机中存储的是二进制,能进行的运算是补码,也就是说下面这些位运算都是基于二进制补码的基础上的。
位运算的运算符如下:
运算符 | 作用 | 示例 |
---|---|---|
~ | 按位取反 | ~0 = 1 ~1 = 0 |
& | 按位与 | 1&1=1 1&0=0 0&0=0 |
| | 按位或 | 1|1=1 1|0=1 0|0=0 |
^ | 按位异或 | 1^1=0 1^0=1 0^0=0 |
<< | 左移运算 | 4<<1=8 |
>> | 右移运算 | 4>>1=2 |
按位取反~是将二进制补码的每一个1变成0,将每个0变成1。假如计算~8的值,以一个字节为例。
8的二进制补码为:0000 1000,而按位取反之后就是 1111 0111,此时观察最高位为1,说明是个负数,由于这个是个负数,我们按照负数补码的运算法则将之变成原码即可。1111 0111的反码为1111 0110,原码为 1000 1001,则该原码为-9。所以~8=-9。
后面的按位与、按位或、按位异或的使用也是与上面一样的道理,运算出来的结果均是补码,观察最高位符号位判断正负再根据正负数求补码的方法逆向求出原码即可。
在此介绍一个快捷的方法,就是对补码进行取反+1的操作可以快速得到一个数的相反数。比如 1111 1101是一个补码,对其取反是 0000 0010,加1后就是 0000 0011,即为3,也就是上述补码的相反数,所以上述的1111 1101是-3。公式如下:
∼ [ A ] 补 + 1 = [ − A ] 补 \sim[A]_补 + 1 = [-A]_补 ∼[A]补+1=[−A]补
关于取反、按位与、按位或、按位异或的示例代码如下:
#if 1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
// 按位取反 ~
void test01()
{
int num = 2;
printf("~num = %d\n", ~num);
}
// 按位与 &
void test02()
{
int num = 520;
if ((num & 1) == 1)
{
printf("num 是奇数\n", num);
}
else
{
printf("num 是偶数\n", num);
}
}
// 按位或
void test03()
{
int num1 = 10;
int num2 = 40;
printf("num1 | num2 = %d\n", num1 | num2);
}
// 按位异或 ^
void test04()
{
int num1 = 67;
int num2 = 45;
//int tmp = num1;
//num1 = num2;
//num2 = tmp;
num1 = num1 ^ num2;
num2 = num1 ^ num2;
num1 = num1 ^ num2;
//num1 = num1 + num2;
//num2 = num1 - num2;
//num1 = num1 - num2;
printf("num1 = %d\n", num1);
printf("num2 = %d\n", num2);
}
int main(int argc, char* argv[])
{
test01();
test02();
test03();
test04();
system("pause");
return 0;
}
#endif
而<<与>>是C语言中的移位运算符,移位运算符就是将所有的位向左或者向右移动,操作的同样是二进制补码。在左移运算中,所有的低位都是用0进行填补的,高位舍弃。如计算127<<3,127的补码为 0111 1111,左移三位即为 1111 1000,高位的011舍弃掉,低位补3个0。而我们进行右移运算的时候,如果是无符号数高位补0;如果是有符号数一般我们左补符号位,也可能是统一左补0,这个依赖于操作的机器,所有有符号数的右移稍微会有些许麻烦,但是一般操作的时候都是添加符号位,这可以可以保证右移后数的正负性不至于被改变。左移运算和右移运算n位相当于是乘以或者除以2的n次幂。