一、内存和指针
1、内存与地址
举个例子,一栋大楼中有许多个房间,如果想要快速的找到一个房间,就需要知道这个 房间的门牌号,这个门牌号就可以理解成这个房间的地址;
在计算机中其实也是把内存划分为⼀个个的内存单元,每个内存单元的⼤⼩取1个字节。
计算机中常⻅的单位(补充):
- ⼀个⽐特位可以存储⼀个2进制的位1或者0;
- 一个字节(byte)等于八个比特位(bit)。
存储信息的内存编号 <==> 地址 <==> 指针
二、指针变量(&与*)
在编写代码时:
int a; 创建了一个整型变量a,向内存中申请了一个整型变量(占4个字节)的空间,每一个字节的内存空间都有它的内存编号。
<1> &(取地址操作符)
通过&(取地址操作符)可以获得一个变量的地址,&a就是a的地址;
有了a的地址,现在需要把地址存起来,这时候就用到了指针变量;
int a; int* b=&a;
这里,b就是一个指针变量,类型是int*,*说明b是一个指针变量,int表示b所指向的对象类型是个整型。
<2> *(解引用操作符)
指针变量存放地址,如果想通过这个地址找到它指向的变量,就需要用到*(解引用操作),即
int a=10;
int* b=&a;
printf("%d",*b);
通过解引用操作,就可以找到指针所指向的变量;
指针变量可以存储一个变量的地址,但如果这个变量是一个指针变量,这是,就会一个新的概念:二级指针
二级指针:存放一级指针地址的变量。
int a=10; int* pa=&a; int** ppa=&pa;
*pa=a。 **ppa=*pa=a。
三、指针类型的意义
<1> 指针类型不同,通过解引用操作所访问从字节数就不同
例:char*类型的指针解引用操作只访问一个字节,二int*类型访问4个字节;
int a = 1234443; int* p = &a; *p = 0;
int a=1234443; char*pa=(char*)&a; *p=0;
<2> 指针类型不同,指针运算所跳过的字节数也不同
char* 类型指针+1跳过1个字节,而int*类型的指针+1跳过4个字节。
<3>void*指针
void是一个泛型指针,它可以接受任何类型的指针,但是不能进行指针运算和解引用操作。
四、指针运算
<1>指针+-整数
我们知道,数组在内存中是连续存储的,所以,只需要找到一个元素的地址,通过指针+-整数会可以访问数组中所有数据。
#include<stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; int i, sz; int* p = arr; sz = sizeof(arr) / sizeof(arr[0]); for (i = 0; i < sz; i++) { printf("%d ", *(p + i)); } return 0; }
用指针加减整数去实现数组下标访问。
<2>指针-指针
指针-指针的绝对值是两个指针之间的元素个数。
前提:两个指针指向同一块内存空间。
#include<stdio.h> int my_strlen(char* str) { char* p = str; while (*str != '\0') { str++; } return str - p; } int main() { char arr[] = "abcd"; printf("%d\n", my_strlen(arr)); return 0; }
<3>指针关系运算
指针地址大小的比较。
int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,0 }; int* p = arr; int sz = sizeof(arr) / sizeof(arr[0]); while (p < arr + sz) { printf("%d ", *p); p++; } return 0; }
五、const 修饰指针
当const修饰一个变量时,这个变量就不可以再进行修改;现在,用const修饰一个指针变量,这个指针变量是不是也是不可被修改的呢?
我们知道,指针变量存放的是地址,地址又有所指向的值,所以用const修饰指针变量,是指针变量不可被修改,还是指针所指向的值不可被修改呢?
<1>const 修饰*p
const int *p;
const 放在*的前面(左面)时,const修饰指针所指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本身的内容可变(就是他存储的地址可以改变)。
<2>const 修饰p
int* const p;
const如果放在*的后面(右边),修饰的是指针变量本身,保证了指针变量的内容不能修改(它存储的地址不能改变),但是指针指向的内容,可以通过指针改变。
六、野指针
<1>野指针形成原因
1.指针没有初始化
int* p; *p=20;
规避:初始化指针;不知道指针该指向哪里,就给指针赋值NULL(空指针)。
NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
2.指针越界
# include <stdio.h> int main () { int arr[ 10 ] = { 0 }; int *p = &arr[ 0 ]; int i = 0 ; for (i= 0 ; i<= 11 ; i++) { // 当指针指向的范围超出数组 arr 的范围时, p 就是野指针 *(p++) = i; } return 0 ; }
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是 越界访问。
3.指针所指向的空间被释放
#include <stdio.h> int* test() { int n = 100; return &n; } int main() { int*p = test(); printf("%d\n", *p); return 0; }
尽量不使用局部变量的地址。
<2>assert断言
指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
assert(p != NULL );
用来判断指针是否为空指针,如果为空,代码程序终止。
assert.h 头⽂件定义了宏 assert() ,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报
错终⽌运⾏。这个宏常常被称为“断⾔”。
而如果不再需要assert进行断言
# define NDEBUG
使assert失效,不进行判断。
七、传值调用与传址调用
当你用代码编写一个函数交换两个数的值时
void swap1(int x, int y) { int t; t = x; x = y; y = t; } int main() { int a, b; scanf("%d%d", &a, &b); printf("交换前:a=%d, b=%d", a, b); swap1(a, b); printf("交换后:a=%d, b=%d", a, b); return 0; }
你会发现,这样并没有把a与b的值进行交换;
其实,这样写代码只是将a与b的数值传给了函数swap1的形参,而在函数运行时,会创建两个临时变量x,y用来接收a,b的值,而函数也只是将x,y两个的值交换了,并没有把a和b进行交换;当函数执行完以后,a与b没有进行交换。
实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实
参。
像这种只是把数值传给函数形参的,就是传值调用
那又该怎样实现?
可以将a,b的地址传给函数,这样函数在运行工程中,可以直接通过解引用操作访问a与b,并进行修改
void Swap2(int*px, int*py) { int tmp = 0; tmp = *px; *px = *py; *py = tmp; } int main() { int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap2(&a, &b); printf("交换后:a=%d b=%d\n", a, b); return 0; }
传址调⽤:可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。
应用:strlen的模拟实现
int my_strlen(const char * str) { int count = 0; assert(str); while(*str) { count++; str++; } return count; } int main() { int len = my_strlen("abcdef"); printf("%d\n", len); return 0; }