指针
知识点
指针的概念
指针保存了一个内存地址,对指针的操作就是对地址的操作。
可以将内存理解为一个“大数组”,指针相当于存储了一个数组下标,它指向下标对应位置的变量。
指针的声明
用以下代码声明指针。其中,&
运算符被称为取地址运算符,它返回变量在内存中的地址。
void指针可以指向任意类型的值。
int a;
int *ptrToA = &a;
double b;
double *ptrToB = &b;
void *voidPtrToA = &a;
void *voidPtrToB = &b;
指针的声明
用以下代码声明指针。其中,&
运算符被称为取地址运算符,它返回变量在内存中的地址。
void指针可以指向任意类型的值。
int a;
int *ptrToA = &a;
double b;
double *ptrToB = &b;
void *voidPtrToA = &a;
void *voidPtrToB = &b;
取地址运算符不能作用于常量或表达式,如 int *ptr = &2;
或者 int *ptr = &(a + b);
。因为他们在内存中并没有固定的地址。
空指针
空指针的值可以用 NULL
(C和C++98的风格)或者 nullptr
(C++11新标准)表示。他们的值都是0x0
,表示指针不指向任何对象。
空指针不可以解引用,对其解引用会出现运行时错误(Runtime Error)。
NULL的定义:
//cite from <stddef.h>
#ifndef __cplusplus
#define NULL ((void *)0) // C语言
#else
#define NULL 0 // C++
#endif
可以发现,C语言的NULL保证为 void*
类型,但C++的NULL仅为常量 0
。在C++中使用NULL宏可能会导致函数重载错误。相比之下, C++11引入的 nullptr
始终保证其为指针类型。
在C++代码中,建议使用 nullptr
。
知识点
指针赋值
使用指针时,假设 ptr
为一个指针。
ptr
的值为指针本身的值,是一个十六进制地址;- 同类型的指针之间可以赋值,如
ptr1 = ptr2
,赋值相当于改变了指针指向的对象。
间接访问
为了访问指针指向的值,我们使用 *
符号,这被称为解引用运算符:
*ptr
的值为指针指向的变量的值;- 对
*ptr
的修改会作用到原对象上。
注意声明int *ptr = &a;
中的 *
并不是解引用运算符,它是类型声明的一部分。(非常非常非常重要的一句话,让我搞错了好长时间)
void类型
void*
指针可以指向任何类型的值。
void*
类型不可以解引用。
事实上,使用 void*
代表着你放弃了所有类型检查和类型安全性。因此,除非必要,不建议在C++代码中使用 void*
类型。此后我们会学到处理任意类型更强大、安全的工具:模板。
我们看一下以下代码的运行结果,加深一下理解:
int a = 233;
int *ptrToA = &a;
void *voidPtrToA = &a;
cout << *ptrToA << endl; // 233
cout << ptrToA << endl; //一个十六进制数,表示内存中的位置。例如0x7ffd99314e64
*ptrToA = 466;
cout << a << endl; // 466
*voidPtrToA = 699; // Compile Error: ‘void*’ is not a pointer-to-object type
int c = 1, *ptrToC = &c;
ptrToA = ptrToC; // 现在ptrToA指向了变量c
reinterpret_cast
对于一个指向类型 A
的指针,我们可以将其转换成一个指向类型 B
的指针。此时,指针指向的位置没有变,只是对于内存中数据的解释方式变了。
转换的方式便是 B *ptr2 = reinterpret_cast<B *> ptr1;
。
如以下代码,表示用一个 float
指针解释一个内存中的 int
变量:
int x = 1;
float *fp = reinterpret_cast<float *> &x;
要注意的是转换后的类型,它的有效长度不能比原来的类型更长。比如说 int
类型为 4 byte,double
类型为 8 byte。将一个指向 int
类型的指针转换成指向 double
类型的指针,这在语法上没有问题,但是如果解引用得到的指针,double
多出的 4 byte 的数据是无意义的。
指针数组
知识点
数组名其实就是指向数组第0个元素的指针。但是,不能修改“数组名”这个指针的值,即它是常量指针。
int a[] = {1, 2, 3};
*a; // 等同于a[0]
*(a + n); // 等同于a[n]
int *p = a + 1;
*p; // 2
*(p + 1); // 3
知识点
当指针指向数组元素时,加减法才有意义。
可以对指针加上或者减去一个整数。这表示:将指针在数组中向前或向后移动若干位置。
当两个指针指向同一个数组时,可以对两个指针做减法。这表示两个指针所指向元素在数组中的距离。
p = p + 1; // *p == 3
p = p - 1; // *p == 2
cout << p - a << endl; // 1
指针在偏移后不能超过数组的范围。当对超过数组范围的指针解引用时,行为未定义(可能出现运行时错误)。
同样,也不要对不在同一个数组内的两个指针执行减法。
//题目描述:
//实现函数`productMinMax`,返回数组中最小值和最大值的积。
//保证结果不会超过int范围。
//输入:无
//输出:`-90`
#include <iostream>
int productMinMax(int *arr, int len)
{
int minV = *arr, maxV = *arr;
for (int *p = arr + 1; p < arr + len; ++p)
{
if (minV > *p) minV = *p;
if (maxV < *p) maxV = *p;
}
return minV * maxV;
}
int main()
{
int arr[] {1, 5, -4, 3, 2, 10, -3, -9, -8};
std::cout << productMinMax(arr, sizeof(arr) / sizeof(int)) << std::endl;
}
动态内存分配
知识点
new Type
在堆(程序的静态存储区)上新建一个对象,返回指向这个对象的指针。
new Type[cnt]
在堆上新建一个长为cnt,类型为Type的数组,返回这个数组的首指针。
new运算符的初始化:
int *p1 = new int; // *p1为不确定的任意值
int *p2 = new int(3); // *p1 == 3
int *p3 = new int(); // *p1 被初始化为0
int *p4 = new int[4]; // p4数组的所有元素为不确定值
int *p5 = new int[4] {1, 2, 3, 4}; // 用花括号列表初始化
int *p6 = new int[4] (); // p6数组的所有元素被初始化为0
int *p7 = new int[4] {1}; // 注意:p7数组仅有第0个元素为1;其余元素都被初始化为0
无论如何,不建议依赖上文中“初始化为0”的语法,这会增加debug的难度。所有new得到的元素应该被显式初始化。
delete ptr
和 delete ptr[]
可以释放ptr对应的内存。
- 如果ptr并不指向
new
的内存,程序会发生运行时错误 - 如果已经
new
的内存未被delete
,则发生内存泄漏
当new操作失败(如系统内存耗尽),它返回nullptr
。使用new时建议检测是否成功。
动态变量实例
知识点
使用传统的数组,我们只能在源代码中就写好数组的大小,不能修改;
通过申请动态数组,我们可以在运行时控制数组的大小。
指针与字符串
知识点
字符串也是储存在数组中的。可以把字符串常量赋值给数组来初始化。
由于字符串的最后一个字节一定为'\0'
,只要得到数组的首指针,就可以遍历整个字符串。
char ss[] = "abc", *str = ss;
char *str1 = new char[10];
strcpy(str1, "abc");
//输出ss,等价于cout << ss << endl;
for (char *p = ss; *p != 0; ++p)
{
cout << *p;
}
cout << endl;
也可以把字符串常量赋给 const char*
指针。因为字符串常量的每一个字符不可以修改,使用了 const
修饰符。但这是较陈旧的用法,不建议使用。如果不使用 const
,严格说是错误的(编译器会提示warning,但可以运行),此时若修改字符串的任一位,行为未定义(undefined)。
const char *s1 = "hello"; // ok, deprecated
char *s2 = "world"; // warning: ISO C++ forbids converting a string constant to ‘char*’
//题目描述:
//实现函数`myStrcmp`,比较两个输入的字符串。
//如果第一个字符串字典序小于第二个,返回-1;如果字典序相同,返回0;如果第一个字符串字典序大于第二个,返回1。
//输入:无
//输出:`-1`
#include <iostream>
int myStrcmp(const char *s1, const char *s2)
{
while (*s1 && *s2)
{
if (*s1 < *s2)
{
return -1;
}
else if (*s1 > *s2)
{
return 1;
}
s1++;
s2++;
}
if (*s1)
{
return 1;
}
else if (*s2)
{
return -1;
}
else
{
return 0;
}
}
int main()
{
char s1[] = "abcde", s2[] = "abcef";
std::cout << myStrcmp(s1, s2) << std::endl;
}
指针与函数
知识点
如何在函数中修改传入变量的值?一种常用的方法是使用指针。
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
int a = 1, b = 2;
swap(&a, &b);
}
求解一元二次方程的函数
知识点
//题目描述:
//请实现函数 `int SolveQuadratic(double a, double b, double c, double *px1, double *px2)`,返回一元二次方程 ax^2 + bx + c = 0 的两个根,并返回不同的根的个数。如果 a=0,返回3。
//输入:无
//输出:
//2
//2 1
#include <iostream>
int SolveQuadratic(double a, double b, double c, double *px1, double *px2)
{
double disc, sqrtDisc;
if(a == 0) return 3;
disc = b * b - 4 * a * c;
if( disc < 0 ) return 0;
if ( disc == 0 ) { *px1 = -b /(2 * a); return 1;}
sqrtDisc = sqrt(disc);
*px1 = (-b + sqrtDisc) / (2 * a);
*px2 = (-b - sqrtDisc) / (2 * a);
return 2;
}
int main()
{
double x1, x2;
std::cout << SolveQuadratic(1, -3, 2, &x1, &x2) << std::endl;
std::cout << x1 << " " << x2 << std::endl;
}
返回指针的函数
知识点
指向全局变量的指针可以作为函数返回值。
但局部变量不可以。局部变量的生命周期为:声明时,直到当前作用域(花括号)结束。函数返回后,该局部变量对应内存被回收,指向它的指针无意义。
指向new得到的内存的指针可以作为返回值。该内存在堆上分配,直到被delete之前都不会被回收。
int tmp;
int *func1 (int val)
{
int a;
int *b = new int [2];
// return &val; // err: val的生命周期在return时就结束了
// return &a; // err: 同上
// return b; // ok
return &tmp; // ok
}
引用概念
知识点
引用可以理解为变量的"别名"。同时,也可以理解为一个type *const
指针,即指针指向的对象的值可变,但指针本身的地址不可变。在指针的基础上,引用省略了取地址和解引用。
对引用的操作(求值,修改等)始终绑定在原对象上。
int a = 1;
int &b = a; // a == 1; b == 1
a = 2; // a == 2; b == 2
b = 3; // a == 3; b == 3
cout << sizeof(b) << endl; // 4; 和sizeof(a)相同
cout << &b << ' ' << &a << ' ' << (&a == &b) << endl; // true; a和b的地址是一样的
int *c = &b; // 此时c指向a
cout << sizeof(c) << endl; // 64位系统上为8; 和上面sizeof(b)做对比
引用传递
知识点
如果想在函数内修改一个对象(而非数组)的值,传引用是更现代的方式。
引用的另一个作用是可以让函数返回多个值。此时,只要传入多个引用,然后把他们当作返回值修改即可。
void swap(int &a, int &b)
{
int tmp = a;
a = b;
b = tmp;
}
void return2value(int &a, int &b)
{ //returns {3, 4}
a = 3, b = 4;
}
int main()
{
int a, b;
return2value(a, b);
swap(a, b);
cout << a << ' ' << b << '\n';
}
即使不需要修改传入对象的值,按引用传递往往也是更高效的方式。此时,常常使用常量引用传递。
这对于大对象的传递尤其有效。按值传递大对象时,会导致对整个对象的拷贝,可能很慢;而按引用传递时,开销仅仅为传一个指针(64 位计算机中为 8 bytes)。
因此,大多数时候,C++的函数参数为常量引用或普通引用,非引用/指针的参数出现较少。
// struct的知识会在后面学到,现在只需要知道BigType是个很大的对象: sizeof(BigType) == 4000
struct BigType
{
int val[1000];
};
void func1(BigType a) // copies 4000 bytes
{
// ...
}
void func2(const BigType &a) // copies 8 bytes
{
// ...
}
//题目描述:
//实现函数`myFindCnt`,返回`vector`数组中某个值`val`第一次出现的下标。
//如果该值不存在,返回数组的长度。
//同时,要在函数的cnt参数中返回这个值出现的次数。
//输入:无
//输出:
//4
//2
#include <iostream>
#include <vector>
using namespace std;
int myFindCnt(const vector<int> &arr, int val, int &cnt)
{
int res = arr.size();
cnt = 0;
for (int i = 0; i < arr.size(); ++i)
{
if (arr[i] == val)
{
if (res == arr.size())
{
res = i;
}
++cnt;
}
}
return res;
}
int main()
{
vector<int> arr = {1, 2, 6, 1, 3, 4, 2, 4, 3};
int cnt;
std::cout << myFindCnt(arr, 3, cnt) << std::endl;
std::cout << cnt << std::endl;
}
知识点
函数也可以返回变量的引用,此时和指针类似。可以返回的引用包括:
- 对全局变量的引用
- 返回函数的引用类型参数
- 返回函数指针类型参数解引用之后的结果
同上,不能返回局部变量的引用。
int glob;
int& func(int a, int &b, int *c)
{
int tmp;
return a; // err
return tmp; // err
return b; // ok
return *c; // ok: *c为引用类型
return glob; // ok
}