指针, 数组, 引用, 常量
1 指针(Pointer)
定义形式:Type* name [= expr]; 推荐在定义时初始化。
初始化约束与常见规则:
- 可初始化为
nullptr
(C++11 起)、0
或NULL
(旧写法); - 可初始化为对象地址(类型必须兼容);
- 指针可被赋值为另一个同类型指针(前提是已初始化)。
示例:
int a = 10;
int* p = &a; // 指向 a
int* n = nullptr; // 空指针
指针运算:p + n
会按照 sizeof(*p)
移动地址;*(p + i)
等价于 p[i]
。注意运算优先级:*(p+n) != *p + n
。
不要解引用无效或越界指针(UB)。
2 数组(Array)
一维数组与指针:数组名在大多数表达式中会退化为指向首元素的指针,但数组本身不是指针,大小信息会丢失。
int a[5] = {1,2,3,4,5};
int* p = a; // 等价于 &a[0]
std::cout << p[2];
多维数组:int a[3][4];
在表达式中退化为 int (*)[4]
(指向含 4 个 int 的数组的指针)。
指针数组与数组指针:
- 指针数组:
int* arr[5];
(数组元素是指针); - 数组指针:
int (*p)[5];
(p 指向一个含 5 个 int 的数组)。
数组引用示例(避免拷贝):
void g(int (&ra)[20]) { /* 直接访问原数组 */ }
注意:字符串字面量以 \0
结尾,定义 char s[5] = "hello";
会越界。
下面将用简洁的示例说明一维数组与多维数组(尤其是二维数组)的常见用法、指针关系和范围遍历。
示例:遍历二维数组的几种写法并说明指针含义
#include <iostream>
using namespace std;
// 使用类型别名表示指向包含 4 个 int 的数组的指针
using int_row = int[4];
void example_pointer_iteration()
{
int a[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
// 方式 1:使用显式指针遍历(int_row* 指向每一行)
for (int_row *p = a; p != a + 3; ++p) {
for (int *q = *p; q != *p + 4; ++q)
cout << *q << ' ';
cout << '\n';
}
// 方式 2:范围 for(推荐,语义清晰)
for (auto &row : a) {
for (auto &elem : row)
cout << elem << ' ';
cout << '\n';
}
}
int main()
{
example_pointer_iteration();
return 0;
}
要点总结:
- 对二维数组
int a[3][4]
,表达式a
的类型是int[3][4]
,在大多数表达式中会退化为int (*)[4]
(指向含 4 个 int 的数组的指针)。 a + 1
指向第二行;*(a + 1)
的类型是int[4]
,在表达式中又可退化为int*
指向该行第一个元素。- 数组名在多数表达式中会退化为指针,但在
sizeof
、取地址&
、以及作为初始值的数组拷贝(constexpr 情况除外)时不退化。
示例:一维数组与“超尾”元素
int arr[4] = {1,2,3,4};
int *p = arr; // 指向 arr[0]
int *q = arr + 4; // 指向“超尾”位置 &arr[4],不能解引用,但用于范围判断是合法的
3 引用(Reference)
引用是已初始化的别名,语法为 T& r = obj;
。引用必须在定义时初始化,初始化后不能重新绑定。引用不能为“空引用”。
引用与指针的主要区别:
- 引用更像别名,使用更安全(不可为 null、不可变绑定);
- 指针可为空、可重新赋值,适合遍历数据结构与可选引用场合。
示例:
int x = 1; int &r = x; r = 2; // x == 2
4 常量与枚举
- 使用
const
声明常量:const int N = 10;
;在文件作用域下非extern
的const
默认为内部链接(每个翻译单元一个拷贝);要跨文件共享请使用extern const
或inline const
(C++17)。 - 枚举:
enum Color { RED, GREEN, BLUE };
。 - 枚举类(scoped enum):
enum class Color { RED, GREEN };
,更安全且不导出名字到外部作用域。
常量折叠:编译器会在编译期计算常量表达式并进行折叠优化,但 const
变量与宏不同,前者仍是语言符号可被类型系统识别。
5 宏(#define)优缺点
缺点:
- 只是文本替换、无类型检查;
- 调试困难、易引入命名冲突;
优点:
- 条件编译、编译期替换与编译器内置宏(如
__FILE__
、__LINE__
)对调试有用。
示例(条件编译):
#define DEBUG
#ifdef DEBUG
// debug code
#endif
优先使用 constexpr
、const
、inline
常量与模板替代宏。
6 数据对齐与对象大小
结构体/类成员会按对齐规则排列,编译器可能插入填充字节以满足对齐要求。对象大小受成员类型、对齐以及是否有虚函数(vptr)影响。空类大小至少为 1 字节。
7 链接性(Linkage)与可见性
- 内部链接(internal linkage):标识符仅在当前翻译单元可见,例如
static
自由函数或命名空间中未显式extern
的const
对象(老规则); - 外部链接(external linkage):标识符可跨翻译单元链接,如普通非
inline
函数与非静态全局变量(定义唯一)。
注意:C++17 引入 inline
变量允许在头文件中定义全局变量而不违反 ODR(One Definition Rule)。
8 函数声明与定义(声明优先)
- 函数应在使用前声明(通常放在头文件),在实现文件定义;
- 声明与定义可以分离,链接时将把声明绑定到唯一的定义;
inline
函数允许在多个翻译单元中定义相同实现(用于头文件)。
示例:
// header.h
void f();
// source.cpp
#include "header.h"
void f(){ /* ... */ }
9 异常、默认参数与调用约定(简要)
- 异常处理为语言级机制,异常传播会进行栈展开并调用析构函数;
- 默认参数在编译期决定,不能作为函数重载的区分条件;
- 调用约定(ABI)决定参数压栈和清栈方是谁,平台相关,通常由编译器控制。
如果你同意,我将按目录顺序继续处理下一个文件 oop6-7.md
(若还未处理则按顺序跳过已处理文件)。
同一个项目中的函数名字表示,限定名不同即可区分;不同项目中的函数名字表示,需要区分;
调用协定
这种协议规定了该语言函数调用时的参数传送方式、参数是否可变和由谁来处理堆栈等问题. 约定有cdecl、stdcall、fastcall、pascal、thiscall等几种
cdecl(调用者)
函数参数按照从右到左的顺序入栈,并且由调用函数者负责把参数弹出栈以清理堆栈
stdcall
函数参数按照从右到左的顺序入栈,没有显式定义非成员函数的调用约定, 并且由被调用的函数负责在返回前清理传送参数的栈,函数参数个数固定
fastcall
用于对性能要求非常高的场合, 通常将函数的从左边开始的两个大小不大于4个字节的参数通过寄存器传递,其余的参数仍旧自右向左压栈传递,并且由被调用的函数负责在返回前清理传送参数的堆栈。
名字重整
编译器通常会在生成源程序的目标文件阶段,将其中具有存储性质的对象的名字按照某种既定规则进行改写
所谓的具有存储性质的对象,即左值对象
函数重载(奥里给)
[!tip]
兼容性原则:
只要有一个变量可以使之重载, 便是合法重载. 重载函数不会满足所有变量的情况.
[!warning]
函数参数的个数、类型、顺序、const修饰、异常等不完全相同 返回值类型不作为区分标志 缺省参数不作为区分标志 值类型参数的const型与非const型不作为区分标志 引用和指针类型参数,是否可以改变实参,可作为区分标志
根据操作数的类型来区分不同的操作,并应用适当的操作,是编译器的责任,而不是程序员的事情. 通过省去为函数起名并记住函数名字的麻烦,函数重载简化了程序的实现,使程序更容易理解
int Func();
int Func(int);
int Func(int,int);
int Func(int,int) const; //只能是成员函数
int Func(int) throw( );
int Func(int) throw(int,MyE,YourE);
int Func(MyClass obj);
函数重载和作用域
重载函数应在同一个作用域中声明。如果局部地声明一个函数,则该函数将屏蔽而不是重载在外层作用域中声明的同名函数
例:
void print( const string& );
void print( double ); //重载
void foobar( int i )
{
extern void print(int); //隐藏print() or int print=0;
print(“Value”); //错误
print(i); //正确
}
重载函数绑定
[!note]
确定候选函数、选择可行函数、寻找最佳匹配.
例:
void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); // calls void f(double, double)
f(42, 2.56); // error
f(static_cast<double>(42), 2.56); // calls f(double, double)
f(42, static_cast<int>(2.56)); // calls f(int, int)
参数表
函数的缺省参数既可以在函数声明也可以在函数定义中指定默认实参。但是,在一个文件中,只能为形参指定默认实参一次.
[!tip]
应在函数声明中指定默认实参,并将该声明放在合适的头文件中。如果在函数定义的形参表中提供默认实参,那么只有在包含该函数定义的源文件中调用该函数时,默认实参才是有效的.
如果一个函数被声明多次并出现在不同的作用域内,则允许分别为它们指定不同的默认实参.
int f(int a=6,float b=5.0,char c='.',int d=10);
int main( )
{
int f(int a=3,float b=2.0,char c='n',int d=20); cout<<f( )<<endl; //f函数使用局部默认实参
return 0;
}
实参和形参的匹配
[!note]
压栈顺序由调用约定确定 但计算顺序是不确定的
错误的例子
int a =10;
f(++a,++a,++a);
func( MyF(a),YourF(a)+6 );
当一个函数有多个形参时,C++语言没有规定函数调用时实参的求值顺序,编译器根据对代码进行优化的需要自行规定对实参的求值顺序 在实参中注意不要使用带有副作用的运算符,此时可能产生二义性.
[!note]
缺省参数的匹配 实参的类型转换
缺省参数:
在编译时匹配,而不是运行时; 第一个带缺省值参数的右侧必须都有缺省值;
void f(int,char=‘c’,int); //非法
void f(int,char=‘c’,int=9);//正确
转换:
若不存在完全匹配的函数,尝试类型转换(每个参数只一次)。
void f(int);
void f(double);
实际调用:f(2.5f);
[!note]
值传递 指针传递 数组的传递 引用传递
数组传递
可将使用数组语法定义的形参看作指向数组元素类型的指针。上面的几种定义是等价的,形参类型都是 int*
int a[3]={1,2,3};
编译器只会检查实参是不是指针、指针的类型和数组元素的类型是否匹配,而不会检查数组的长度
以下几个等价:
void f(int[ ]);
void f(int a[ ]);
void f(int a[3]);
void f(int a[5]);
void f(int * a);
//二维
void fun(int array[5][10]); //第一维的大小通常省略
void fun(int array[][10], int i); //fun(int array[][]);是错误的 仅能省略第一维
void fun(int (*array)[10], int i);
void fun(int *array[5], int j);
void fun(int *array[], int i, int j);
void fun(int **array, int i, int j);
引用传递
引用对实参的要求
[!caution]
不允许传递一个右值或具有需要转换的类型的对象(类型不匹配的对象) 非const 引用形参只能与完全同类型的非const 对象关联 应该传入一个有效的对象
[!tip]
为避免限制对函数的使用,应该将不修改相应实参的形参定义为 const 引用
返回类型
值/指针
返回引用的函数返回一个左值。因此,这样的函数可用于任何要求使用左值的地方.
函数link数组
理解 int * (*T[5])(void);
这种复杂的C++声明需要一步步分析。我们可以从内到外逐层解读每个部分的含义。为了更清晰地解释,我们可以遵循以下步骤:
步骤 1: 基本语法结构
从最内层开始,逐步解释并扩展:
T[5]
: 这是一个数组声明。T
是一个包含 5 个元素的数组。(*T[5])
: 这表明数组T
中的每个元素是一个指针。(*T[5])(void)
: 进一步表明,T
中每个指针指向的是一个函数,该函数没有参数(void
表示函数没有参数)。int * (*T[5])(void)
: 最外层表明,这些函数返回一个int
类型的指针。
步骤 2: 分解和重组
让我们从里到外一步步解析 int * (*T[5])(void);
:
T[5]
: 声明T
是一个包含 5 个元素的数组。*T[5]
: 数组中的每个元素是一个指针。(*T[5])(void)
: 每个指针指向一个接受void
参数的函数。int * (*T[5])(void)
: 每个函数返回一个指向int
类型的指针。
因此,int * (*T[5])(void);
声明了一个包含 5 个元素的数组 T
,其中每个元素是一个指向函数的指针,这些函数不接受参数并返回一个指向 int
的指针。
更具体的解释
为了更好地理解这种声明,可以考虑以下具体的例子:
int* func1(void) {
static int x = 10;
return &x;
}
int* func2(void) {
static int y = 20;
return &y;
}
int* func3(void) {
static int z = 30;
return &z;
}
int* func4(void) {
static int w = 40;
return &w;
}
int* func5(void) {
static int v = 50;
return &v;
}
int* (*T[5])(void) = { func1, func2, func3, func4, func5 };
int main() {
for (int i = 0; i < 5; ++i) {
int* result = T[i](); // 调用 T 数组中的函数
std::cout << "Result from func" << i+1 << ": " << *result << std::endl;
}
return 0;
}
在这个例子中:
- 定义了5个函数
func1
到func5
,每个函数返回一个指向int
的指针。 - 声明了一个数组
T
,并将这5个函数的地址赋给数组T
的每个元素。 - 在
main
函数中,通过调用T
中的每个函数,打印返回的int
值。
视觉化理解
如果用图表来表示 int * (*T[5])(void);
的结构,它可以如下图所示:
T:
+-----------+ +-----------+ +-----------+ +-----------+ +-----------+
| T[0] | | T[1] | | T[2] | | T[3] | | T[4] |
| function* | | function* | | function* | | function* | | function* |
+-----|-----+ +-----|-----+ +-----|-----+ +-----|-----+ +-----|-----+
| | | | |
v v v v v
+---|---+ +---|---+ +---|---+ +---|---+ +---|---+
| func1 | | func2 | | func3 | | func4 | | func5 |
+---|---+ +---|---+ +---|---+ +---|---+ +---|---+
| | | | |
v v v v v
+---|---+ +---|---+ +---|---+ +---|---+ +---|---+
| int* | | int* | | int* | | int* | | int* |
+-------+ +-------+ +-------+ +-------+ +-------+
小结
T
是一个指针数组:T
是一个包含 5 个元素的数组。- 每个元素是一个函数指针:数组中的每个元素是一个指向函数的指针。
- 这些函数无参数并返回
int
指针:每个函数都不接受参数,并返回一个指向int
类型的指针。
理解这些复杂的声明,需要分解每个部分,逐步解析和组合,才能彻底理解其含义。