内存地址
- 字节:字节是内存的容量单位,英文称为 byte,一个字节有8位,即 1byte = 8bits
- 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址,系统通过对应的内存地址从而定位内存的位置
基地址
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
- 多字节数据:对于多字节数据而言,期地址是其所有字节中编号最小的那个,称为基地址。
取址符
- 每个变量都是一块内存,都可以通过取址符 & 获取其地址
- 例如:
int a = 100;
printf("整型变量 a 的地址是: %p\n", &a);
char c = 'x';
printf("字符变量 c 的地址是: %p\n", &c);
double f = 3.14;
printf("浮点变量 f 的地址是: %p\n", &f);
-
注意:
- 虽然不同的变量的尺寸是不同的,但是他们的地址的尺寸确实一样的,在32为系统为4字节,64位系统为8字节
- 不同的地址虽然形式上看起来是一样的,但由于他们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的。
指针基础
-
指针的概念:
- 一个专门用来存放内存地址的变量,指针也就是指针变量
-
地址。比如 &a 是一个地址,也是一个指针,&a 指向变量 a
- 专门用于存储地址的变量,又称指针变量。
-
格式
- 类型 *指针变量名
- 解释:
- “类型” : 指针变量指向的内存空间存放的数据类型
- “指向” : 如果我保存了你的地址,那么就说我指向你
- “*” :定义指针变量的固定格式
// 系统中给a申请了4个字节的内存空间 int a = 10; printf("a addr:%p\n",&a); // 定义一个指针变量用于存放a的地址 int *p = &a; // 第一部分:*p :首先p是一个变量,占用内存8个字节,存放了a的地址 // 第二部分:int 指的是指针变量所指向的内存空间放了什么类型的数据 printf("p的值:%p addr : %p\n",p,&p); printf("a的值:%d addr : %p\n",a,&a);
-
指针的定义:
// 用于存储 int 型数据的地址,p1 被称为 int 型指针,或称整型指针
int *p1;
// 用于存储 char 型数据的地址,p2 被称为 char 型指针,或称字符指针
char *p2;
// 用于存储double型数据的地址,p3 被称为 double 型指针
double *p3;
- 指针的赋值:赋给指针的地址,类型需跟指针的类型相匹配。
int a = 100;
p1 = &a; // 将一个整型地址,赋值给整型指针p1
char c = 'x';
p2 = &c; // 将一个字符地址,赋值给字符指针p2
double f = 3.14;
p3 = &f; // 将一个浮点地址,赋值给浮点指针p3
- 指针的索引:通过指针,取得其指向的目标
*p1 = 200; // 将 p1 指向的目标(即a)修改为200,等价于 a = 200;
*p2 = 'y'; // 将 p2 指向的目标(即c)修改为'y',等价于 c = 'y';
*p3 = 6.6; // 将 p3 指向的目标(即f)修改为6.6,等价于 f = 6.6;
-
指针的尺寸
- 指针尺寸指的是指针所占内存的字节数
- 指针所占内存,取决于地址的长度,而地址的长度则取决于系统寻址范围,即字长
- 结论:指针尺寸只跟系统的字长有关,跟具体的指针的类型无关
- 在32位系统,指针的大小占用4字节
- 在64位系统,指针的大小占用8字节
// 指针大小 int a = 10; char b = 'c'; float c = 85.5; char *p1 = &b; int *p2 = &a; float *p3 = &c; printf("p1 size : %ld\n",sizeof(p1)); printf("p2 size : %ld\n",sizeof(p2)); printf("p3 size : %ld\n",sizeof(p3));
野指针
-
概念:指向一块未知区域的指针,被称为野指针。野指针是危险的。
-
危害:
- 引用野指针,相当于访问了非法的内存,常常会导致段错误(segmentation fault)
- 引用野指针,可能会破坏系统的关键数据,导致系统崩溃等严重后果
-
产生原因:
- 指针定义之后,未初始化
- 指针所指向的内存,被系统回收
- 指针越界
-
如何防止:
- 指针定义时,及时初始化
- 绝不引用已被系统回收的内存
- 确认所申请的内存边界,谨防越界
#include <stdio.h>
int main(int argc, char const *argv[])
{
// 野指针,如果定义定义指针没有指向空间,则称为野指针
// 野指针是危险的,所以定义指针的时候如果不确定指向对应的空间
// 则可以先指向NULL
//int *p;// 野指针
int *p = NULL; // NULL表示(void *)0
return 0;
}
空指针
很多情况下,我们不可避免地会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针所指向的内存被释放了等等。一般的做法就是将这些危险的野指针指向一块确定的内存,比如零地址内存。
- 概念:空指针即保存了零地址的指针,亦即指向零地址的指针。
- NULL地址其实就是 (void *)0,就是0
- 示例:
// 1,刚定义的指针,让其指向零地址以确保安全:
char *p1 = NULL;
int *p2 = NULL;
// 2,被释放了内存的指针,让其指向零地址以确保安全:
char *p3 = malloc(100); // a. 让 p3 指向一块大小为100个字节的内存
free(p3); // b. 释放这块内存,此时 p3 相当于指向了一块非法内存
p3 = NULL; // c. 让 p3 指向零地址
指针运算
- 指针加法意味着地址向上移动若干个目标
- 指针减法意味着地址向下移动若干个目标
- 示例:
int a = 100;
int *p = &a; // 指针 p 指向整型变量 a
int *k1 = p + 2; // 向上移动 2 个目标(2个int型数据)
int *k2 = p - 3; // 向下移动 3 个目标(3个int型数据)
#include <stdio.h>
int main(int argc, char const *argv[])
{
int a[5] = {1,2,3,4,5};
int *p = a;
// 指针偏移1个目标,不是偏移1个字节
printf("%p\n",(p+1));
// 几乎不用,容易出现越界
int (*q)[5] = &a;
printf("%p\n",(q+1));// 越界
printf("%d\n",*(q+1));
return 0;
}
#include <stdio.h>
int main(int argc, char const *argv[])
{
int a[5] = {1,2,3,4,5};
int *p = a;
printf("%d\n",*(p+1));
// 数组指针
int (*q)[5] = &a;
printf("%d\n",**q);
// q指向的是整个数组的地址
// q+1是偏移这个数组+1,即偏移int [5]共20字节
printf("%d\n",*(*(q+1)));
return 0;
}
- 指针运算
int a = 10;
//int *p = &a;// 初始化
int *p;
// 其它时候用来表示 *单独使用为解引用,所以 *p = &a; 错误
p = &a;
printf("a value:%d\n",a);
printf("*(&a) value:%d\n",*(&a));// * 与 &是互逆运算
printf("%d\n",*p);
*p = 300; //*p 此时相当于变量a
printf("a value:%d\n",a);
-
指针偏移
- 指针的加减称为指针的偏移,偏移的单位由指针的类型大小所决定所谓指针的类型大小指的是指针变量所指向的内存空间的数据类型
-
数组与指针
-
数组名有两个含义
- 第一个含义,表示整个数组
printf("%ld\n",sizeof(a));
- 第二个含义,表示首元素地址
int *p = a;
-
数组下标
int a[3] = {1,2,3}; int b = a[0]; int c1 = *(a+0); // a[0] int c2 = *(a+1); // a[1] int c3 = *(a+2); // a[2] int d1 = *(0+a); // a[0] int d2 = *(1+a); // a[1] int d3 = *(2+a); // a[2] printf("2[a] = %d\n",2[a]); // *(2+a) 仅限面试用 int a[3] = {1,2,3}; printf("%d,%d,%d,%d\n",a[0],*(a+0),*(0+a),0[a]); // *的作用就是[]相当于解引用 printf("%d,%d,%d,%d\n",a[1],*(a+1),*(1+a),1[a]); return 0;
总结:数组最后编译器会自动转为指针操作,数组运算其实就是指针运算
-
-
指针转数组
int b[10];
// 指针没有让它指向对应的空间,会出现段错误
int *a = b;
a[0] = 1;
printf("%d\n",a[0]);
// 如果是指针操作,会修改原有空间的内容
// 指针与原空间为同一块空间
int a[3] = {3,4,5};
int *p = a;
p[1] = 10;
printf("%d,%d,%d\n",*(p+1),p[1],a[1]);
// 空间不属于同一块空间
int b = 19;
int q = b;
q = 18;
printf("%d,%d\n",b,q);
练习1:
int array[5] = {10,20,30,40,50};
要求定义一个指针p存储数组的名字,通过数组名或者指针将所有获取到40数据的方法罗列出来
#include <stdio.h>
int main(int argc, char const *argv[])
{
int array[5] = {10,20,30,40,50};
int *p = array;
printf("%d,%d\n",*(p+0),p[0]);
printf("%d,%d\n",*(p+1),p[1]);
printf("%d,%d\n",*(p+2),p[2]);
printf("%d,%d\n",*(p+3),p[3]);
printf("%d,%d\n",*(p+4),p[4]);
return 0;
}
总结 :
1.指针一定要指向一块合法的空间,否则出现段错误,没有空间自行分配
2.可以将指针转换成数组使用,如上所示
- 指针偏移量
int a[2] = {1,2};
int *p0 = &a[0];
printf("p0 addr:%p:%d,p0+1 addr:%p:%d\n",p0,*p0,p0+1,*(p0+1));
char *p = (char *)&a[0];
printf("p addr:%p:%d,p+1 addr:%p:%d\n",p,*p,p+1,*(p+1));
char b[5] = {'a','b','c','d','e'};
int *q = (int *)&b[0];
printf("%c\n",*(q+1));
练习3:
有这么一个十六进制数据int data = 0x11223344,定义指针指向data
然后通过指针把22打印出来
#include <stdio.h>
int main(int argc, char const *argv[])
{
int data = 0x11223344;
char *p = (char *)&data;
printf("%x\n",*p);
printf("%x\n",*(p+1));
printf("%x\n",*(p+2));
printf("%x\n",*(p+3));
return 0;
}
-
字节序
- 大端模式:高位数据存放在内存的低地址端,低位数据存放在内存的高地址端
- 小端模式:高位数据存放在内存的高地址端,低位数据存放在内存的低地址端
练习4:判断linux系统到底是大端模式存放数据,还是小端模式存放数据
#include <stdio.h> int main(int argc, char const *argv[]) { int data = 0x12345678; char *p = (char *)&data; if(*p == 0x78) { printf("小端模式\n"); } else { printf("大端模式\n"); } return 0; }
练习5
输入字符串ch,并作出以下修改 1.输入a或者A,输出* 2.如果输入b或者B,输出# 3.其它数据保持不变,如果字符串第一个字符ch[0]为x,则退出 #include <stdio.h> int main(int argc, char const *argv[]) { char ch[100] = {0}; while(1) { scanf("%s",ch); if(ch[0] == 'x') break; int len = sizeof(ch) / sizeof(ch[0]); for(int i = 0; i < len; i++) { if(ch[i] == 'a' || ch[i] == 'A') { ch[i] = '*'; } if(ch[i] == 'b' || ch[i] == 'B') { ch[i] = '#'; } } printf("%s\n",ch); } return 0; }
-
数组与指针转换
- 数组指针
数组的指针的本质为指针,此指针保存的是数组的地址,说白了就是,指针指向数组名,此类指针称为数组指针
int arr[3][4] = {{1,2,3,4},{10,20,30,40},{11,22,33,44}};
定义一个指针p存储数组的名字//数组指针 int a = 5;
int (*p)[4] = arr;
通过arr p 将所有获取到1的数据的方法罗列出来
通过arr p 将所有获取到20的数据方法罗列出来
int (*p)[4] = arr;
int (*q)[3][4] = &arr;
int arr[3][4] = {
{1,2,3,4},
{10,20,30,40},
{11,22,33,44}
};
//定义一个指针p存储数组的名字//数组指针
int (*p)[4] = arr;
//通过arr p 将所有获取到1的数据的方法罗列出来
printf("arr[0][0] = %d\n",p[0][0]);
printf("arr[0][0] = %d\n",(*(p+0))[0]);
printf("arr[0][0] = %d\n",*((*(p+0))+0));
printf("arr[0][0] = %d\n",**p);
//通过arr p 将所有获取到20的数据方法罗列出来
printf("arr[1][1] = %d\n",p[1][1]);
printf("arr[1][1] = %d\n",(*(p+1))[1]);
printf("arr[1][1] = %d\n",*((*(p+1))+1));
// 数据类型不匹配,因为二维数组不是二级指针
// 以下是错误的写法
// int **p1 = array;
// printf("%d,%d,%d\n",*(*(p1+1)+1),(*(p1+1))[1],p1[1][1]);
int a[4] = {1,2,3,4};
int (*q)[4] = &a;
// 越界
//printf("%d\n",*(*(q+1)));
练习2:
int array[3][5] = {
{10,20,30,40,50},
{11,22,33,44,55}
{110,120,130,140,150}
};
要求定义一个指针p存储数组的名字,通过数组的名字或者指针 将 所有获取到11数据的方法都罗列出来
- 指针数组
概念:
指针的数组,的本质为数组,数组里面存放的内容为指针,而一般指针是指向的地址为字符串居多,我们把此类型称为指针数组
定义:
char *p[5];
char *p = "jack";
char *p[5] = {"jack","rose","ken","tony","tom"};
printf("%s\n",p[2]);
printf("%c\n",p[3][3]);
#include <stdio.h>
int main(int argc, char const *argv[])
{
char strs[3][5] = {"jack","rsoe","ken"};
char *str = "jack";
// 错误的,str指向的是只读常量区,常量区的数据无法修改
// *(str+1) = 'o';
char *str1 = "rose";
char *str2 = "ken";
char *buf[3] = {str,str1,str2};
printf("%s\n",buf[0]);
printf("%s\n",buf[1]);
printf("%s\n",buf[2]);
char *buf1[3] = {"jack","rose","ken"};
printf("%s,%c\n",buf1[1],buf1[1][2]);
return 0;
}
"hello"是字符串常量,同时"hello"也是一个匿名数组的空间首地址
char *str[3] = {"abc","def","hij"};
-
字符串与指针
-
字符串常量在内存中实际就是一个匿名数组
-
匿名数组满足数组的两个条件
1.第一个含义,表示整个数组
char a[10]; sizeof(a);
2.第二个含义,表示首元素地址
char buf[] = "abcd"; printf("%c\n",buf[1]); printf("%c\n","abcd"[1]); char *p = "abcd"; // 将p指向一块匿名数组的一个首地址 printf("%p,%p\n","abcd",&"abcd"[0]);
// 讲"jack"字符串存放在buf的空间里面 char buf[] = "jack"; // 定义指针p指向buf的首地址 char *p = buf; // 定义指针q指向"jack"的首地址 char *q = "jack"; printf("%s,%s,%s,%d\n",buf,p,q,sizeof(q));
-
常见问题
- 问:数组是不是就是地址?
- 答:有时候是,有时候不是。在C语言中非常重要的一点是:同一个符号,在不同场合,有不同的含义。
比如数组int a[3];
当出现在以下三种情形中的时候,它代表的是一块12字节的内存: - 初始化语句时:
int a[3];
- 与
sizeof
结合时:sizeof(a)
- 与取址符
&
结合时:&a
只有在上述三种情形下,数组a
代表一片连续的内存,占据12个字节,而在其他任何时候,数组a
均会被一律视为其首元素的地址。
因此,不能武断地说数组是不是地址,而要看它出现的场合。
- 问:指针不是地址吗?为什么还可以取地址?地址的地址是什么意思?
- 答:你这个疑惑是典型的概念混淆。首先需要明确,指针通常指指针变量,是一块专用于装载地址的内存,因此指针跟别的普通变量没什么本质区别,别的变量可以取地址,那么指针变量当然也可以取地址。
// 一般不会用指针直接指向字符串常量,可以用数组或者自行分配的堆空间
char buf2[5] = "jack";
buf2[1] = 'o';
printf("%s\n",buf2);
// 错误的,&buf2是数组指针类型,不能用二级指针指向
//char **ptr = &buf2;
char *ptr = buf2;
char **ptr1 = &ptr;
printf("%s\n",*ptr1);
找的一些题目,难度不大
将一个字符串逆序输出 “abcd” —》dcba
#include <stdio.h>
#include <string.h>
int main(int argc, char const *argv[])
{
// 原数据不能修改
char str[5] = "jack";
// 申请一个临时变量(空间)存放
//int len = sizeof(str) / sizeof(char) - 1;
int len = strlen(str);
printf("%d\n",len);
char temp[5] = {0};
for(int i = 0,j = len-1; j >= 0; j--,i++)
temp[i] = str[j];
printf("%s\n",temp);
return 0;
}
思考:如何用递归的方法实现呢?同志们?
编写一个程序,清除用户输入的字符串空格,并输出,例如输入"a b",输出"ab";
#include <stdio.h>
int main(int argc, char const *argv[])
{
char str[100] = {0};
int len = 0; // 记录输入的字符长度
// 不断的输入字符,直到输入回车结束
while((str[len++] = getchar()) != '\n');
// 将'\n'改为'\0'
str[--len] = '\0';
printf("len : %d\n",len);
for(int i = 0; i < len; i++)
{
// 找空格
if(str[i] == ' ')
{
// 空格后面的所有内容往前移动
for(int j = i; j < len; j++)
{
str[j] = str[j+1];
}
}
}
printf("str : %s\n",str);
return 0;
}
拓展题;
华为笔试题:字符串去重 aabbccddeeff---->abcdef
这道题我之前的博客中有解答过,大家可以翻一翻
哪个地方显示有问题的可以留言,我会更新修改