内存地址
将内存抽象成一个很大的一维字符数组,编码就是对内存的每一个字节分配一个32位或64位的二进制编号。这个内存编号称之为内存地址(唯一),内存中的每一个数据都会分配相应的地址。
#include<iostream> using namespace std; int main() { int num = 10; cout << &num << endl; cout << sizeof(&num); //32位系统分配32位编码,4个字节; //64位系统分配64位编码,8个字节; return 0; }
注意:这里的地址是8个字节
地址用16进制的数表示,一位16进制数表示4位的二进制数,2^4 = 16
如:8:1000 9:1001 A:1010 B:1011
指针和指针变量
指针:和int,double等类似,是一种独立的数据类型。这种类型的变量存储的值是内存地址。
指针变量:本质就是变量,只是该变量存储的是内存地址,而不是普通的数据。不同类型的指针变量所占用的存储单元长度是相同的。
#include<iostream> using namespace std; int main() { int* p1 = nullptr; int num = 10; cout << sizeof(num) << endl;//这个表示num的数据类型int在电脑中占4个字节 cout << sizeof(p1) << endl;//指针,内存地址8个字节 cout << sizeof(*p1) << endl;//加了一个*,解引用,表示指针所指向的int类型的值 cout << &p1 << endl;//存储这个整型指针的地址 cout << sizeof(&p1) << endl; cout << &(*p1) << endl;//指针所指向的地址 return 0; }
野指针
指的是没有被初始化过的指针。指针变量未初始化时不会自动成为 nullptr,而是一个随机值。
int main() {
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使用
return 0;
}
因此,为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就会直接报错,产生非法内存访问。
悬空指针
悬空指针,指针最初指向的内存已经被释放了的一种指针。(指针操作超越变量的作用域,比如函数返回栈内存的指针或引用)
int main() {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr。此时再使用,编译器会直接保错。
野指针和悬空指针,无法通过简单地判断是否为 nullptr 避免,所以要习惯在初始化时赋值或析构时赋为nullptr。
void*
是一种特殊的指针类型,可以存放任意对象的地址,但不能直接操作void*
指针所指的对象。
指针与数组名的区别
-
修改内容上的差别:
char a[] = "hello";
a[0] = 'H';
char *p = "world"; // p 指向常量字符串,该字符串存储在文字常量区,不可更改
// p[0] = "W" // 所以这个赋值有问题
“world” 是const char*类型,不可更改
如果要改用下面的数组先定义,然后再改。
int main() {
char a[] = "hello";
char *p = a; // 这样让指针指向数组 a,而非常量字符串,就可以修改了
p[0] = 'H';
a[1] = 'E';
printf("%d", sizeof(p)); // 64 位机器
std::cout << p << std::endl;
return 0;
}
//输出:8HEllo
数组指针与指针数组
type (*p)[]; // 数组指针
/*
type a[],a 是一个数组,数组内元素都是 type 类型,将 a 换成 (*p),
可以理解为 (*p)是一个数组,数组内元素都是 type 类型,那么 p 就是指向这样的数组的指针,即数组指针。
*/
type *p[]; // 指针数组
/*
type *p[]即(type *)p[],则 p 是一个 type* 类型的数组(类比int a[10],a 是 int 型数组)
即 p 是一个数组,数组内元素都是指针( type *)
*/
const int seq_cnt = 6;
vector<int> *seq_addrs[seq_cnt] = {
&fibonacci, &lucas, &pell,
&triangular, &square, &pentagonal
};
// seq_addrs 是一个数组,存储 vector<int> * 型的指针,
// seq_addrs[0] 的内容为指针,该指针指向 fibonacci,
// 而 fibonacci 的类型为 vector<int>。
EssentialC++ 29
指针函数与函数指针
指针函数:返回类型是指针的函数,如
char *StrCat(char *ptr1, char *ptr2)
{
char *p;
do something;
return p;
}
函数指针:指向函数的指针,如
#include <stdio.h>
int max(int x, int y)
{
return x > y? x: y;
}
int main()
{
// int max(int x, int y);
int (*ptr)(int, int);
ptr = max;
int max_num = (*ptr)(12, 18); // int max_num = ptr(12, 18);也是对的
printf("%d", max_num);
return 0;
}
// 打印 18。
int (*ptr)(); // “()” 表明该指针指向函数,函数返回值为 int
int (*ptr)[3]; // “[]” 表明该指针指向一维数组,该数组里包含三个元素,每一个元素都是int类型。
数组名指向了内存中一段连续的存储区域,可以通过数组名的指针形式去访问,也可以定义一个相同类型的指针变量指向这段内存的起始地址,从而通过指针变量去引用数组元素。
每一个函数占用一段内存区域,而函数名就是指向函数所占内存区的起始地址的函数指针(地址常量),故函数指针解引用与不解引用没有区别 。通过引用函数名这个函数指针让正在运行的程序转向该入口地址执行函数的函数体,也可以把函数的入口地址赋给一个指针变量,使该指针变量指向该函数。
解读复杂指针使用右左法则:首先从未定义的标识符所在的圆括号看起,然后往右看,再往左看。每当遇到圆括号就调转阅读方向。一旦解析完圆括号里的东西,就跳出圆括号。重复这个过程,直到整个声明解析完毕。
int (*func)(int *p, int (*f)(int*));
// func左边有一个*表明func是一个指针,跳出圆括号看右边,右边有括号,说明func是一个函数指针,
// 指向的函数接收两个形参,分别是整型指针 int * 和函数指针 int (*f)(int*),返回值为 int。
int (*func[5])(int *p);
// func 是一个数组,含5个元素,左边*号,表明 func 是一个指针数组(由于[]优先级高于*,func先跟[]结合,然后*修饰func[5],故该数组的元素都是指针)。
//再往右看,是括号,说明 func 里的指针是函数指针,函数指针所指向的函数接收 int * 型参数并返回 int。
int (*(*func)[5])(int *p);
// func 是一个指针,指向含有 5 个元素的数组,数组里的元素都是指针,而且都是函数指针,
// 函数指针指向的函数接收 int * 型形参并返回 int。
int (*(*func)(int *p))[5];
// func 是一个指针,该指针指向一个函数,该函数接收 int * 型参数返回一个指针,
// 返回的指针指向一个数组,该数组含有 5 个元素,每一个元素都是 int 型。
常引用作为函数参数
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用
void foo(const string &s) {
cout << s << endl;
}
// 如果形参里的 const 去掉,程序就报错,
// 因为 ss 与 "world!" 都是常量,你不能把一个 const 类型转换成非 const 类型。
// 所以 foo() 形参必定要用 const 修饰。
int main() {
const string ss("hello ");//此处有const,且“hello”本来就是一个常量
foo(ss);
foo("world!");
}
对于常量类型的变量,其引用也必须是常量类型的;对于非常量类型的变量,其引用可以是非常量的,也可以是常量的。但是要注意,无论什么情况都不能使用常量引用修改其引用的变量的值。