一、指针
- 访问内存是否只能通过变量来进行访问
我们知道程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段存储空间?
我们来看以前访问内存空间的方法:
int a = 10;
a = 20;
变量a占用4个字节的内存空间,我们要想改变这个段内存空间的值,需要将这个变量的值改成20,这个是通过变量的别名来访问内存的方法。我们再来看看一段代码:
#include <stdio.h>
void test_pointer1()
{
int a = 10;
int* pa = &a;
a = 20;
printf("a = %d\n", a);
*pa = 30;
printf("a = %d\n", a);
}
int main()
{
test_pointer1();
return 0;
}
来看看运行的结果:
可以看到我们通过*pa和a进行赋值是等效的。
- 指针的本质
(1) 指针在本质上也是一个变量
(2) 指针需要占用一定的内存空间
(3) 指针用于保存内存地址的值
我们来理解一下:
首先任何一种数据类型都可以理解为一种变量,指针也是一样的,其次既然指针是一种变量,那它就具有内存空间;既然指针也有内存空间,那它的内存空间应该要存放数据,这个数据就是和他对应变量内存的地址。我们来看看一段代码:
void test_pointer2()
{
int b = 10;
int* pb = &b;
printf("sizeof(pb) = %d\n", sizeof(pb));
printf("pb = 0x%x\n", pb);
}
运行结果:
我们可以看到指针的大小为8(有可能是4,取决于编译器是32位还是64位),指针的值是变量b的地址。
我们来想一下,既然指针也有大小,那是不是不同类型的指针大小不一样,我们来看一段代码:
struct C {
int a;
int b;
short c;
};
void test_pointer3()
{
int a = 10;
char b = 20;
struct C c;
c.a = 30;
c.b = 20;
c.c = 10;
int* pa = &a;
char* pb = &b;
struct C* pc = &c;
printf("sizeof(pa) = %d, sizeof(pb) = %d, sizeof(pc) = %d\n", sizeof(pa), sizeof(pb), sizeof(pc));
}
这里我们分别看看int char和struct C类型的指针的大小,运行结果:
可以看到不同类型的指针大小都是8,也就是说指针的大小是固定的。
- 指针*的含义
(1) 在指针声明时,,*号表示所声明的变量为指针 号表示所声明的变量为指针
(2)在指针使用时,,*号表示取指针所指向的内存空间中的值
注意:指针声明的时候可以不初始化,但是一般设置初始值为NULL。
*号类似一把钥匙 号类似一把钥匙,通过这把钥匙可以通过这把钥匙可以打开内存,,读取内存中的值和设置内存的值。
- 形参中的传值和传址调用
(1) 指针是变量,,因此可以声明指针参数
(2) 当一个函数体内部需要改变实参的值,则需要使用指针参数 (3)函数调用时实参值将复制到形参
(4) 指针适用于复杂数据类型作为参数的函数中
我们来看一下传值和传址的区别:
void test_pointer4(int* m, int n)
{
*m = 10;
n = 20;
}
int main()
{
int m = 1;
int n = 1;
test_pointer4(&m, n);
printf("m = %d, n = %d\n", m, n);
return 0;
}
运行结果:
可以看到传值调用没法改变变量的值,传址调用可以改变变量的值,这是因为传值调用的时候函数内部访问的是栈上的数据,调用完就销毁了,传址调用函数内部直接访问内存地址,可以直接修改地址的值。
- void*
前面我们了解到指针也是一种变量,具有内存和地址,那么void也是一种数据类型,也是具有大小和地址的。**void类型可以指向任何一种数据类型的地址**,我们来看一段代码:
struct STD{
char name[20];
int number;
};
void test_pointer5(void* std)
{
struct STD* s = (struct STD*)std;
printf("name: %s\n", s->name);
printf("number: %d\n", s->number);
}
int main()
{
struct STD std;
stpcpy(std.name, "xiaoming");
std.number = 10;
test_pointer5(&std);
return 0;
}
运行结果:
我们可以看到作为形参的void*可以指向struct STD类型,这个在数据封装中是非常有用的。
- const和指针结合
const修饰指针:
修饰指针有几种情况,我们先看看以下代码:
const int* p; //p可变,p指向的内容不可变
int const* p; //p可变,p指向的内容不可变
int* const p; //p不可变,p指向的内容可变
const int* const p; //p和p指向的内容都不可变
可以看到const修饰谁,谁就是不可变的,比如const int* p,const修饰int类型,标明p指向的内容不可变,int* const p,const修饰p,标明p不可以变,也就是地址不可变。
我们看一个代码例子:
#include <stdio.h>
void test_const()
{
const int a = 10;
//a = 20; //报错, 对常量赋值
int b = 10;
int c = 20;
const int* p1; //p可变,p指向的内容不可变
int const* p2; //p可变,p指向的内容不可变
int* const p3; //p不可变,p指向的内容可变
const int* const p4; //p和p指向的内容都不可变
p1 = &b;
*p1 = 11; //报错,p指向的内容不可变
p2 = &b;
*p2 = 11; //报错,p指向的内容不可变
p3 = &b;
p3 = &c; //报错,p不可变
p4 = &b;
*p4 = 11; //报错,p指向的内容不可变
p4 = &c; //报错,p不可变
}
int main()
{
test_const();
return 0;
}
编译的时候可以看到:
编译器遵循被const修饰的指针规则
- 指针的运算
在讲指针运算的时候我们要始终理解指针就是一种数据类型,它具有地址和大小。
我们先来看看指针的加减运算:
void test_pointer6(void)
{
int a;
int* pa = &a;
printf("pa: 0x%x\n", pa);
pa++;
printf("pa: 0x%x\n", pa);
pa--;
printf("pa: 0x%x\n", pa);
}
运行结果:
可以看到指针加1的时候pa的值加了4,减1的时候pa减4,这样我们可以得到:
p +(-) n; <==> (unsigned int)p +(-) n*sizeof(*p);
结论:
当指针p指向一个同类型的数组的元素时 :p+1 将指向当前元素的下一个元素;p-1将指向当前元素的上一 个元素
指针之间的运算:
void test_pointer7(void)
{
int a;
int b;
int* pa = &a;
int* pb = &b;
int diff = 0;
printf("pb: 0x%x\n", pb);
printf("pa: 0x%x\n", pa);
diff = pb - pa;
printf("diff: %d\n", diff);
}
运行结果:
这里可以看到指针相减并不是直接两个地址的的值直接相减,还需要除以数据类型的大小:
p1 – p2; <==>( (unsigned int)p1 - (unsigned int)p2) / sizeof(type);
注意:
(1)指针之间只支持减法运算,且参与运算的指针类型必须相同
(2)只有当两个指针指向同一个数组中的元素时,指针指针相减才有意义,其意义为指针所指元素的下标差
(3)当两个指针指向的元素不在同一个数组中时,结果未定义
二、数组
-
数组的概念
数组是相同类型的变量的有序集合
int a[5];
-
数组的大小
(1)数组在一片连续的内存空间中存储元素
(2)数组元素的个数可以显示或隐式指定
int a[5] = {1,3};
int b[] = {2,4};
我们来想两个问题:
a[2], a[3], a[4]的值是多少? b包含了多少个元素?
直接来一段代码:
void test_array()
{
int a[5] = {1, 3};
int b[] = {2, 4};
printf("sizeof(a) = %d\n", sizeof(a));
printf("a[2] = %d, a[3] = %d, a[4] = %d\n", a[2], a[3], a[4]);
printf("sizeof(b) = %d\n", sizeof(b));
printf("b[0] = %d, b[1] = %d, b[2] = %d\n", b[0], b[1], b[2]); //这里可能会出错,出错的原因是内存越界
}
看看结果:
可以得到结论,数组没有被初始化的元素被系统初始化为0,数组的大小为数组sizeof(type) x num,type是数组元素的数据类型,num为数组的元素个数。
- 数组地址和数组名
(1)数组名代表数组首元素的地址
(2)数组的地址需要用取地址符&才能得到
(3)数组首元素的地址值与数组的地址值相同
(4)数组首元素的地址与数组的地址是两个不同的概念
我们来看一段代码:
void test_array2()
{
typedef int(AINT5)[5];
int a[5] = {1, 2, 3, 4, 5};
AINT5* pa = &a;
pa++;
printf("a = 0x%x, &a[0] = 0x%x\n", a, &a[0]);
printf("pa++ = 0x%x\n", pa);
}
数组本质上讲还是一种数据类型,比如int a[5],它的数组类型是int [5],这样我们也可以定义一个指针来指向它:typedef int(AINT5)[5];
我们来看看运行的结果:
可以看到a和&a[0]是一样的,指向数组a的指针加1的时候,值变化了0x14,也就是5 x sizeof(int),我们可以得到结论是数组也是可以看做一种数据类型,类型为type [num],type为基本数据,num为数组元素个数。
结论:
(1)a为数组是数组首元素的地址
(2) &a为整个数组的地址
(3)a和&a的意义不同其区别在于指针运算
a + 1<> (unsigned int)a + sizeof(*a)
&a + 1<> (unsigned int)(&a) + sizeof(*&a)
-
数组的本质
(1)数组是一段连续的内存空间
(2) 数组的空间大小为sizeof(array_type) * array_size
(3) 数组名可看做指向数组第一个元素的常量指针 -
数组的访问
(1)以下标的形式访问数组中的元素
(2)以指针的形式访问数组中的元素
void test_array3()
{
int a[5] = {1, 2, 3, 4, 5};
a[0] = 2;
a[4] = 1;
*(a+0) = 2;
*(a+4) = 1;
}
- 数组作为形参
C语言中,,数组作为函数参数时, 编译器将其编译成对应的指针
void f(int a[]); <> void f(int* a);
void f(int a[5]); <> void f(int* a);
void test_array4(int a[5])
{
printf("sizeof(a) = %d\n", sizeof(a));
}
运行结果:
可以看到数组作为形参时被转换成了指针,指针的大小就是8或者4
三、总结
(1)指针和数组都可以看做一种数据类型,指针指向对应的数据类型,数组的数据类型是type [num]
(2)指针声明时只分配了用于容纳指针的4或者8字节空间
(3)在作为函数参数时,数组参数和指针参数等价
(4)数组名在多数情况可以看做常量指针, 其值不能改变
四、代码分享:
#include <stdio.h>
struct C {
int a;
int b;
short c;
};
struct STD{
char name[20];
int number;
};
void test_pointer1()
{
int a = 10;
int* pa = &a;
a = 20;
printf("a = %d\n", a);
*pa = 30;
printf("a = %d\n", a);
}
void test_pointer2()
{
int b = 10;
int* pb = &b;
printf("sizeof(pb) = %d\n", sizeof(pb));
printf("pb = 0x%x\n", pb);
}
void test_pointer3()
{
int a = 10;
char b = 20;
struct C c;
c.a = 30;
c.b = 20;
c.c = 10;
int* pa = &a;
char* pb = &b;
struct C* pc = &c;
printf("sizeof(pa) = %d, sizeof(pb) = %d, sizeof(pc) = %d\n", sizeof(pa), sizeof(pb), sizeof(pc));
}
void test_pointer4(int* m, int n)
{
*m = 10;
n = 20;
}
void test_pointer5(void* std)
{
struct STD* s = (struct STD*)std;
printf("name: %s\n", s->name);
printf("number: %d\n", s->number);
}
void test_pointer6(void)
{
int a;
int* pa = &a;
printf("pa: 0x%x\n", pa);
pa++;
printf("pa: 0x%x\n", pa);
pa--;
printf("pa: 0x%x\n", pa);
}
void test_pointer7(void)
{
int a;
int b;
int* pa = &a;
int* pb = &b;
int diff = 0;
printf("pb: 0x%x\n", pb);
printf("pa: 0x%x\n", pa);
diff = pb - pa;
printf("diff: %d\n", diff);
}
void test_array1()
{
int a[5] = {1, 3};
int b[] = {2, 4};
printf("sizeof(a) = %d\n", sizeof(a));
printf("a[2] = %d, a[3] = %d, a[4] = %d\n", a[2], a[3], a[4]);
printf("sizeof(b) = %d\n", sizeof(b));
printf("b[0] = %d, b[1] = %d, b[2] = %d\n", b[0], b[1], b[2]); //这里可能会出错,出错的原因是内存越界
}
void test_array2()
{
typedef int(AINT5)[5];
int a[5] = {1, 2, 3, 4, 5};
AINT5* pa = &a;
pa++;
printf("a = 0x%x, &a[0] = 0x%x\n", a, &a[0]);
printf("pa++ = 0x%x\n", pa);
}
void test_array3()
{
int a[5] = {1, 2, 3, 4, 5};
a[0] = 2;
a[4] = 1;
*(a+0) = 2;
*(a+4) = 1;
}
void test_array4(int a[5])
{
printf("sizeof(a) = %d\n", sizeof(a));
}
int main()
{
int m = 1;
int n = 1;
struct STD std;
int a[5];
stpcpy(std.name, "xiaoming");
std.number = 10;
test_pointer1();
test_pointer2();
test_pointer3();
test_pointer4(&m, n);
printf("m = %d, n = %d\n", m, n);
test_pointer5(&std);
test_pointer6();
test_pointer7();
test_array1();
test_array2();
test_array4(a);
return 0;
}
makefile:
CC = gcc
CFLAGS = -g -Wall -O
main:test.o
$(CC) $^ -o $@
%.o:%.c
$(CC) $(CFLAGS) -c $^
clean:
rm -rf test.o