P21 指针的基本概念
指针与普通变量的区别就是他多了一个解引用,而C语言实现解引用的本质原因是因为指针里面保存了一个地址,解引用的本质就是取出指针中保存地址当中的那个值。
在堆栈开辟的指针使用完之后直接置为NULL即可,而在指向堆的指针需要不在使用free掉之后再将指针置为NULL
等号左边是左值,等号右边是右值
memcpy第二个参数无所谓你什么类型,他最终都会给你转成char*类型
int a = 10;
00911F2F mov dword ptr [ebp-0Ch],0Ah
char* p1 = (char*)&a;
00911F36 lea eax,[ebp-0Ch]
00911F39 mov dword ptr [ebp-18h],eax
int* p = (int*)a;
00911F3C mov eax,dword ptr [ebp-0Ch]
00911F3F mov dword ptr [ebp-24h],eax
p = (int*)p1;
00911F42 mov eax,dword ptr [ebp-18h]
00911F45 mov dword ptr [ebp-24h],eax
由上可知,强制类型转换只是为了欺骗编译器,他们本身的值依旧不变
unsigned char buffer[1024] = { 0 };
00401F32 push 400h
00401F37 push 0
00401F39 lea eax,[buffer]
00401F3F push eax
00401F40 call _memset (0401181h)
00401F45 add esp,0Ch
int a = 0xaabbccdd;
00401F48 mov dword ptr [a],0AABBCCDDh
memcpy(buffer + 1, &a, 4);
00401F52 push 4
00401F54 lea eax,[a]
00401F5A push eax
00401F5B lea ecx,[ebp-407h]
00401F61 push ecx
00401F62 call _memcpy (0401500h)
00401F67 add esp,0Ch
int num = *(buffer + 1);
00401F6A movzx eax,byte ptr [ebp-407h]
00401F71 mov dword ptr [num],eax
printf("%x\n", *(buffer + 1));
00401F77 movzx eax,byte ptr [ebp-407h]
00401F7E push eax
00401F7F push offset string "%x\n" (0409BD4h)
00401F84 call __empty_global_delete (04014F1h)
00401F89 add esp,8
printf("%x\n", *(int*)(buffer + 1));
00401F8C mov eax,dword ptr [ebp-407h]
00401F92 push eax
00401F93 push offset string "%x\n" (0409BD4h)
00401F98 call __empty_global_delete (04014F1h)
00401F9D add esp,8
}
如果将unsigned char改成char buffer:
其实0xffffffdd也就是0xdd是编译器为了提高效率进行了优化
movsx是对有符号数进行的拓展,movzx是对无符号数进行拓展
而溢出是指:
mov al,0x10
add al,0xFF
超过了最大容量溢出来了,而0xdd显然没有溢出
P23 指针的间接赋值
指针的类型决定了指针的步长,指针永远存储的都是一个数据的首地址,编译器怎么知道应该从首地址取多少字节的数据也取决于指针的类型。
P24 指针的输入输出特性
从lea ecx,[arr]可以看出:数组名作函数参数就会退化为指向数组首元素的指针,相当于将&arr[0]作为函数参数进行传递。
int iArr[] = { 1,2,3,4,5 };
00B61AAF mov dword ptr [iArr],1
00B61AB6 mov dword ptr [ebp-18h],2
00B61ABD mov dword ptr [ebp-14h],3
00B61AC4 mov dword ptr [ebp-10h],4
00B61ACB mov dword ptr [ebp-0Ch],5
//字符串默认是const char*类型的数据
const char* arr[] = {
"aaa",
00B61AD2 mov dword ptr [arr],offset string "aaa" (0B69B30h)
"bbb",
00B61AD9 mov dword ptr [ebp-30h],offset string "bbb" (0B69B34h)
"ccc",
00B61AE0 mov dword ptr [ebp-2Ch],offset string "ccc" (0B69BD8h)
"ddd"
00B61AE7 mov dword ptr [ebp-28h],offset string "ddd" (0B69BDCh)
};
int len = sizeof(arr) / sizeof(arr[0]);
00B61AEE mov dword ptr [len],4
printStringArray(arr, len);
00B61AF5 mov eax,dword ptr [len]
00B61AF8 push eax
00B61AF9 lea ecx,[arr]
00B61AFC push ecx
00B61AFD call std::basic_ostream<char,std::char_traits<char> >::_Sentry_base::_Sentry_base (0B6150Ah)
00B61B02 add esp,8
}
运行结果:
输出特性:被调函数分配内存,主调函数使用内存
交换函数:
void doChange(int* x, int* y) {
int tmp = 0;
tmp = *x;
*x = *y;
*y = tmp;
}
int x = 10;
int y = 20;
doChange(&x, &y);
int tmp = 0;
00721995 mov dword ptr [tmp],0
tmp = *x;
0072199C mov eax,dword ptr [x]
0072199F mov ecx,dword ptr [eax]
007219A1 mov dword ptr [tmp],ecx
*x = *y;
007219A4 mov eax,dword ptr [x]
007219A7 mov ecx,dword ptr [y]
007219AA mov edx,dword ptr [ecx]
007219AC mov dword ptr [eax],edx
*y = tmp;
007219AE mov eax,dword ptr [y]
007219B1 mov ecx,dword ptr [tmp]
007219B4 mov dword ptr [eax],ecx
判断以下程序是否存在问题:
#include<iostream>
using namespace std;
#include<string>
#pragma warning(disable:4996)
void getMem(char* p, int n) {
p = (char*)malloc(n);
}
int main() {
char* p = NULL;
getMem(p, 100);
strcpy(p, "hello");
cout << p << endl;
free(p);
return 0;
}
运行结果:
正确写法:
void allocSpace(char** tmp) {
char* p = (char*)malloc(100);
memset(p, 0, 100);
strcpy(p, "hello world");
*tmp = p;
}
void test() {
char* p = NULL;
allocSpace(&p);
printf("%s\n", p);
}
总结:想要通过函数修改当前栈中的变量里保存的值,一定要传递该变量的地址
如果传递指针的目的就是为了打印,即不修改他的内容,那么应当在前面添加const关键字,如果你传递指针的目的就是为了修改变量的话,就不用加const了。
但是这样也是只能防止你无意间修改了传递过来的变量,如果你故意去修改也是完全可以实现的。
读取文件一行使用的fgets()函数:
通俗来讲的话,fgets()
函数的作用就是用来读取一行数据的。但要详细且专业的说的话,fgets()
函数的作用可以这么解释:从第三个参数指定的流中读取最多第二个参数大小的字符到第一个参数指定的容器地址中。在这个过程中,在还没读取够第二个参数指定大小的字符前,读取到换行符'\n'
或者需要读取的流中已经没有数据了。则提前结束,并把已经读取到的字符存储进第一个参数指定的容器地址中。
在正常情况下fgets()
函数的返回值和它第一个参数相同。即读取到数据后存储的容器地址。但是如果读取出错或读取文件时文件为空,则返回一个空指针。
fgets()
函数的运行流程大概是这样子的:
当系统调用这个函数的时,系统便会阻塞等待用户的输入,直到用户输入回车符’\n’才返回程序。然后用户输入的内容会被系统放进输入缓存区里面,fgets()函数便会进来读取其“第二个参数减1(为什么减1后面说)”个字节存进它第一个参数指向的内存地址中,如果在还没读取够需要的字节大小前读取到换行符’\n’则提前返回。
小案例:将文件当中的内容读取到申请的堆空间当中
#include<stdio.h>
#include<iostream>
using namespace std;
#pragma warning(disable:4996)
int getFileLines(FILE* fp) {
char buffer[1024] = { 0 };
int lines = 0;
while (fgets(buffer, 1024, fp) != NULL) {
++lines;
}
//恢复文件指针
fseek(fp, 0, SEEK_SET);
return lines;
}
void getFileData(FILE* fp , char** pArray) {
char buffer[1024] = { 0 };
int line = 0;
while (fgets(buffer, 1024, fp) != NULL) {
int len = strlen(buffer) + 1;
char* pData = (char*)malloc(len);
if (!pData) {
return;
}
strcpy(pData, buffer);
pArray[line++] = pData;
memset(buffer, 0, 1024);
}
}
void printString(char** pArray,int lines) {
for (int i = 0; i < lines; ++i) {
printf("%s\n", pArray[i]);
}
}
//释放堆内存
void freeAllocMem(char*** pArray,int lines) {
for (int i = 0; i < lines; i++) {
if ((*pArray)[i] != NULL) {
free((*pArray)[i]);
(*pArray)[i] = NULL;
}
}
free(*pArray);
*pArray = NULL;
}
void test() {
//打开文件
FILE* fp = fopen("./test.txt", "r");
if (!fp) {
printf("文件打开失败!\n");
return;
}
//获取文件行数
int lines=getFileLines(fp);
cout << lines << endl;
//将文件内容放到申请的堆空间中
char** pArray = (char**)malloc(sizeof(char*) * lines);
getFileData(fp, pArray);
//打印缓冲区数据
printString(pArray, lines);
//释放堆内存
freeAllocMem(&pArray, lines);
cout << pArray << endl;
}
int main() {
test();
return 0;
}
出现如下错误:
很明显是数组的下标越界
这里编译器理解为先pArray[i]然后再取*,应当修改为先取*,即先(*pArray),然后(*pArray)[i]
free(pArray),free掉的是pArray指向的地址,而不是pArray本身的地址!!!
pArray[i]是指pArray指向的内存地址里面保存的内容
我们可以利用位运算来对C语言的个别位进行操作,以实现对变量的精确控制
奇数的最后一位是1,偶数最后一位是0:
void test() {
//判断一个数的奇偶性
int num = 127;
if ((num & 1 )== 0) {
printf("%d是偶数\n", num);
}
else {
printf("%d是奇数\n", num);
}
//将一个数置零
num &= 0;
printf("num=%d\n", num);
}
异或运算的特性:
移位运算:
0101 5
1010 10 ,所以左移几位相当于*2^n
注:1、数组名是一个常量指针 2、数组的下标可以是负数 arr[-1]会被翻译成*(arr-1)
打印二维数组:
//方法一
void printDoubleArr(int(*arr)[3], int len1, int len2) {
for (int i = 0; i < len1; ++i) {
for (int j = 0; j < len2; ++j) {
printf("%d ", *(*(arr + i) + j));
}
printf("\n");
}
printf("\n");
}
//方法二
void printDoubleArr2(int arr[3][3], int len1, int len2) {
for (int i = 0; i < len1; ++i) {
for (int j = 0; j < len2; ++j) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
printf("\n");
}
void test() {
//打印二维数组
int arr[3][3] = {
{1,2,3},
{4,5,6},
{7,8,9}
};
printDoubleArr(arr, 3, 3);
printDoubleArr2(arr, 3, 3);
}
选择排序:
1、对int类型的数据进行排序
void printArr(int arr[], int len) {
for (int i = 0; i < len; ++i) {
printf("%d ", arr[i]);
}
printf("\n");
}
void selectSort(int *arr, int len) {
for (int i = 0; i < len-1; ++i) {
int minIndex = i;
for (int j = i + 1; j < len; ++j) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
swap(arr[i], arr[minIndex]);
}
}
printArr(arr, len);
}
void test() {
//数组名就是指向首元素类型的指针
//接收数组类型参数,就看数组名是什么类型就定义什么类型的形参
int arr[] = {
1,3,7,9,4,5,6,8,2
};
int len = sizeof(arr) / sizeof(int);
printArr(arr, len);
selectSort(arr, len);
}
2、对字符串类型的数据进行排序
void printArr(const char** arr, int len) {
for (int i = 0; i < len; i++) {
printf("%s ", arr[i]);
}
printf("\n");
}
void selectSort(const char** arr, int len) {
for (int i = 0; i < len - 1; ++i) {
int minIndex = i;
for (int j = i + 1; j < len; ++j) {
if (strcmp(arr[j], arr[minIndex]) < 0) {
minIndex = j;
}
}
if (minIndex != i) {
swap(arr[i], arr[minIndex]);
}
}
printArr(arr, len);
}
void test() {
const char* arr[] = {
"ggg","ddd","ccc","bbb","aaa"
};
int len = sizeof(arr) / sizeof(char*);
printArr(arr, len);
selectSort(arr, len);
}
1、数组名就是指向首元素类型的指针
2、接收数组类型参数,就看数组名是什么类型就定义什么类型的形参
3、通过函数操作数组直接可以修改数组中的数据,因为传递的是指向首元素的指针!
指针可以操作:普通类型的数据、数组、结构体、函数
结构体数组的使用:
struct Person
{
char name[64];
int age;
};
void printPerson(const Person* person, int len) {
for (int i = 0; i < len; ++i) {
printf("Name:%s Age:%d\n", person[i].name, person[i].age);
}
}
void test() {
//在栈上分配结构体数组空间
Person personArr[] = {
{"aaa",20},
{"bbb",30},
{"ccc",40}
};
int len = sizeof(personArr) / sizeof(Person);
printPerson(personArr, len);
//在堆上分配结构体数组空间
//在堆上分配的连续内存空间指针可以直接当数组使用
Person* ps = (Person*)malloc(sizeof(Person) * 6);
if (!ps) {
printf("申请堆内存失败!\n");
return;
}
memset(ps, 0, sizeof(Person) * 6);
for (int i = 0; i < 6; ++i) {
sprintf(ps[i].name, "Name_%d", i + 1);
ps[i].age = 100 + i;
}
printPerson(ps, 6);
free(ps);
ps = NULL;
}
浅拷贝:直接给结构体赋值
直接给结构体赋值,本质上是逐字节拷贝
同一块内存空间释放两次,程序就会荡掉!!!
浅拷贝存在的问题:
如果结构体内部有指针,并且指针指向一段堆空间,那么如果发生赋值行为的时候就会出现两个问题:1、同一块堆空间被释放两次 2、发生内存泄漏。
即如果结构体内部有指针指向堆内存,就不能够使用编译器默认的赋值方式,而应当手动去赋值,就是深拷贝。