指针和引用的区别

  1. 引用可以说是别名,指针有自己的存储空间,里面存储的是所指对象的地址
  2. 引用必须初始化,指针不必须初始化
  3. 引用初始化后不能更改引用的对象,指针初始化后可以更改指向的对象
  4. 两者在汇编层面没有区别
  5. 使用sizeof运算符看指针是4字节(32位机器)或8字节(64位机器),而用sizeof运算符看引用则取决于被引用对象的大小
  6. 指针可以进行自增或自减操作,可以访问原对象相邻存储空间的内容,引用只能固定引用
  7. 返回动态内存分配的对象或内存,必须用指针,用引用有可能内存泄露
  8. 从面向对象的角度,引用不是对象,指针是对象

数组和指针的联系、区别

  1. 指针是单个空间,存储的是所指对象的地址;数组可以有若干单位的空间,存储对象本身;可以有指向数组的指针,也可以有每个单元都是指针的数组
  2. 指针间接访问对象,得先解引用,指针可以直接访问
  3. 当指针指向数组时,可以用自增自减在数组元素上移动,但若要访问还是需要解引用
  4. 当数组作为函数参数传入时,会自动变化为指针,指向数组首位
  5. 不能对数组名直接复制,但可以对指针直接复制
  6. 用运算符sizeof可以得到数组字节数(数组大小),但只能得到指针类型本身的字节数
  7. 内置的下标运算符的索引值不是无符号类型,这与vector这种STL容器不一样,所以内置的下标运算符支持负值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void testArray(int a[]) {
cout << *(a) << endl; // 输出1
cout << *(a+1) << endl; // 输出2
cout << *(a+2) << endl; // 输出3
cout << *(a+3) << endl; // 输出任意值
}
int a[3] = {1,2,3};
testArray(a);

int i = ia[2];
int *p = ia; // p 指向首元素
i = *(p+2); // 等价于i = ia[2]
int *p = &ia[2]; // p指向索引为2的元素
int k = p[-2]; // p[-2]是ia[0]表示的那个元素

int *ip[4]; // 整型指针的数组
int (*ip)[4]; // 指向含有4个整数的数组

void print(const int*);
void print(const int[]);
void print(const int[10]); // 以上三种形式等价,维度为10仅表示程序员的期望,实际不一定

多维数组

  • 严格来说,没有多维数组,应该是数组的数组
1
2
3
int ia[3][4]; // 大小为3的数组,每个元素是含有4个整数的数组
int (*p)[4] = ia; // p指向含有4个整数的数组
p = &ia[2]; // p指向ia的最后一个元素
  • 初始化,嵌套花括号是非必需的
1
2
3
4
5
6
7
8
int ia[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; // 同上等价
int ia[3][4] = {{0}, {4}, {8}}; //仅初始化每行首元素,其他元素默认值初始化
int ix[3][4] = {0,3,6,9}; // 显式初始化每行首元素

函数指针

  • 函数指针是指向函数的指针,声明如:int (*p)(int a, int b);
  • 每个函数在编译时都有一个入口地址,这在汇编代码里面看的十分清楚,函数指针存储的就是这种入口地址的值,可以直接用函数指针调用函数
  • 声明:int (*a)();,注意不能写成int *a();,否则编译器会认为一个名为a的函数的返回值是int*
  • 赋值:直接将一个已经定义的函数名,赋值给函数指针就可以:a = function;
  • 调用:a();,不能直接像定义一个函数一样定义一个函数指针,必须先声明,再给它赋值一个已经定义好的函数名
  • 函数名传递作为函数参数时,会自动退化成一个函数指针
  • 示例:精准解析,在命令行工具如sed、awk中,为了精准解析命令,在结构体里面搞一个函数指针,匹配不同参数,就找不同的函数来执行不同的功能,这比大段的ifelse优雅多了
  • 示例:回调函数,如linux下的signal函数,捕捉某个信号,执行某个信号处理函数,这里要执行的信号处理函数就是用的函数指针
  • 示例:反射,通过字符串直接调用函数,提高代码灵活性和扩展性,Java提供,C++本身不提供,一些类库可能会提供,比如Qt的QMetaObject::invokeMethod("function_name");
  • C++11后多用std::function,可读性大大提升
  • C++11可以直接用auto关键字来声明函数指针,auto f = funcname
1
2
3
4
5
6
7
8
9
10
11
12
int function() { // 正确的函数声明
return 0;
}
int (*a)() { // 错误:这是一个变量,不能当函数一样定义
return 0;
}
int main() {
a = function;
int (*a)(); // 声明一个函数指针变量a,
a(); // 通过函数指针调用
int (*b)() = function; // 直接把声明和赋值写在一起
}

函数指针有可能会出现很复杂的情况,循序渐进的理解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 最简单的函数及其对应的函数指针:
void f();
void (*f_ptr)();
// 复杂点的,带返回值和参数列表,但都是基本类型
int f(double b, int i);
int (*f_ptr)(double b, int i);
// 返回值和参数带上指针,再加上几个const混淆一下
const double * f(const double * b2, int m);
const double * (*f_ptr)(const double * b2, int m);
// 再复杂一点点,参数里加个函数指针 也不是很复杂,基本只要把函数名换成(*函数名) 就可以了
int f(int (*fp)(),int a );
int (*f_ptr)(int (*fp)(),int a );
// 稍微再复杂一点点,返回值是一个函数指针:(光是普通函数返回函数指针,语法就有点费劲。我们一步一步来:)
//// 首先搞一个返回void的普通函数:
void f();
//// 假设返回一个函数指针,这个函数指针返回值和参数都为空。我们用一个函数指针替换掉返回值void就可以了
//// 感觉应该写成这样:void (*fp)() f();
//// 但是这个样子显然过不了编译的,得要变一下:
void (* f())(); //这就是一个参数为空,返回函数指针的函数。
void (*(*f_ptr)())(); //把f替换成(*f_ptr),这就成了返回函数指针的函数指针。

这样显然太过抽象,还好我们可以用typedef关键字把函数指针简化,像普通的int double那样去操作

1
2
3
4
5
6
7
8
9
10
11
void (*f_ptr)(); // 这是定义了一个名为f_ptr的函数指针「变量」
typedef void (*f_ptr)(); // 这是定义了一个名为f_ptr的函数指针「类型」,这个类型代表返回值为空,参数为空的函数指针类型。

void (*fp)() = func; // 将函数名func『赋值』给fp这个函数指针
f_ptr fp = func; // 化简写法

int f(int (*fp)(), int a); // 函数参数是函数指针的写法
int f(f_ptr fp, int a); // 化简写法

void (*(*f_ptr)())() = f; // 将函数名f『赋值』给f_ptr,f_ptr是返回函数指针的函数指针
f_ptr (*ff)() = f; // 化简写法

或者使用尾置返回类型

1
auto f1(int) -> int(*)(int*, int); //f1函数返回一个函数指针

或者结合decltype

1
2
string::size_type sumLength(const string&, const string&);
decltype(sumLength) *getFcn(const string&); // 牢记decltype返回的函数类型,而不是指针类型,所以要显式地加上*

再把数组扯进来

1
2
3
4
5
6
7
8
void (*f_ptr[10])();    // 定义一个长度为10的数组,数组中的元素类型是函数指针
f_ptr[3] = function; // 每一个元素都可以指向一个函数,我们赋值给第数组中的第四个元素函数function的地址
f_ptr[3](); // 通过数组下标拿到函数指针,通过函数指针调用函数。 这里相当于调用了function();

typedef void (*f_ptr)();
f_ptr f_tpr_arrya[10]; //把f_ptr当做一种类型后,声明函数指针数组,就可声明普通的int数组看上去没啥区别了。
f_tpr_arrya[3] = function;
f_tpr_arrya[3]();

类的静态成员函数指针,因为静态成员函数存储方式与普通函数一样,可以取得该函数在内存中的实际地址

  • 声明:void (*static_fptr)();
  • 调用:static_fptr();
  • 赋值:void (*static_fptr)() = &Test::staticFunc;

类的成员函数指针,普通成员函数必须提供this指针

  • 声明:void (Test::*fptr)();,类成员函数指针的声明,就必须加上类名限定,这就声明了一个函数指针变量fptr,他只能指向Test类的成员函数。
  • 赋值:fptr = &Test::function
  • 调用:类的成员函数是无法直接调用的,必须要使用对象或者对象指针调用(这样函数才能通过对象获取到this指针)。
    • (t.*fptr)();,t是Test类的一个实例,通过对象调用。
    • (pt->*fptr)();,pt是一个指向Test类对象的指针,通过指针调用。

虚函数指针,同上,但是虚函数是面向多态的,所以基类的成员函数指针可以赋值给派生类的成员函数指针,坑很多,详见虚函数指针揭秘

没法搞出指向构造函数和析构函数的函数指针,因为构造和析构函数不能被取地址

char*与char[]的区别

  • char []定义的是一个字符数组,注意强调是数组,数组内容可以改变
  • char * 定义的是一个字符串指针,注意强调是指针,指针内容是可以改变的,即可以指向其他地址,但不能改变指针所指向的内容!

关于它们的字节大小与相互之间的转换,仔细体会以下例子(已测试):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char* str = "hello there"; // "hello there"是字面值常量,不可修改!
char carray[] = "hello again"; // "hello again"是字符数组,可以修改!

// 下行输出:8, 11。在64位系统指针占8字节,strlen计算字符串长度时要除去末尾的`\0`,一共11字节
cout << "sizeof(str): " << sizeof(str) << ", strlen(str): " << strlen(str) << endl;
// 下行输出12, 11。末尾的`\0`也算作数组的长度
cout << "sizeof(carray): " << sizeof(carray) << ", strlen(carray): " << strlen(carray) << endl;

// *str = 'H'; // ERROR
// carray = str; // ERROR
str = carray; // carray是字符数组,数组名即指向数组首位的指针,可以赋值给str
cout << str << endl; // 输出:hello again
*carray = 'H'; // carray是字符数组,数组名即指向数组首位的指针,可以通过指针修改数组内容
cout << str << endl; // 输出:Hello again
carray[1] = 'E'; // carray是字符数组,可以通过数组offset修改数组内容
cout << str << endl; // 输出:HEllo again
1
2
3
4
const char * arr = "123";   // 字符串123保存在常量区,本来就是常亮,const加不加都可以
char * brr = "123"; // brr与arr指向的地址相同
const char crr[] = "123"; // 数组,在栈上,长度是4(包括结尾的空字符
char drr[] = "123"; // 另一个数组,drr与crr指向的地址不同,长度也是4