指针与引用
约 10857 字大约 36 分钟
2025-06-25
指针
指针
在C++中,指针(Pointer) 是一种特殊的变量,它存储的不是数据本身,而是另一个变量的内存地址。通过指针,我们可以间接访问和修改它所指向的变量的值。指针是C++的核心特性之一,在信奥赛中广泛应用于动态内存管理、数组操作、函数参数传递等。
一、指针的基本概念
指针的基本概念
1. 内存地址
计算机内存被划分为一个个字节,每个字节都有一个唯一的编号(即地址),用于标识其在内存中的位置。例如,一个int
类型变量通常占用4个字节,其地址是第一个字节的编号。
2. 指针的作用
指针通过存储变量的地址,实现对变量的“间接访问”。想象指针是一个“门牌号”,通过这个门牌号可以找到对应的“房间”(变量)并修改其中的内容。
二、指针的定义与初始化
指针的定义与初始化
1. 定义指针变量
指针变量的定义需要指定它所指向的变量类型(即“基类型”),语法:
数据类型 *指针名;
*
表示这是一个指针变量。- 数据类型是指针所指向的变量的类型(如
int
、double
、struct
等)。
示例:
int *p; // 定义一个指向int类型变量的指针p
double *dp; // 定义一个指向double类型变量的指针dp
char *cp; // 定义一个指向char类型变量的指针cp
2. 初始化指针(赋值地址)
指针必须指向一个已存在的变量才能使用,通过取地址运算符(&) 获取变量的地址并赋值给指针:
int a = 10;
int *p = &a; // p存储a的地址(p指向a)
&a
表示取变量a
的内存地址。- 此时,
p
和&a
的值相同(都是a
的地址)。
三、指针的解引用(访问指向的变量)
指针的解引用(访问指向的变量)
通过解引用运算符(*) 可以访问指针所指向的变量的值,语法:
*指针名; // 表示指针指向的变量
示例:
#include <iostream>
using namespace std;
int main() {
int a = 10;
int *p = &a; // p指向a
cout << "a的值:" << a << endl; // 直接访问a:10
cout << "a的地址:" << &a << endl; // a的地址(如0x7ffd6b6c)
cout << "p存储的地址:" << p << endl; // p存储的地址(与&a相同)
cout << "p指向的值:" << *p << endl; // 解引用p,获取a的值:10
// 通过指针修改a的值
*p = 20; // 等价于 a = 20
cout << "修改后a的值:" << a << endl; // 输出20
return 0;
}
关键:*p
等价于 p
所指向的变量(上例中即a
),对*p
的操作就是对a
的操作。
四、指针的核心特性
指针的核心特性
1. 指针的类型必须与指向的变量类型一致
int a = 10;
double *p = &a; // 错误:double*指针不能指向int类型变量
2. 指针可以改变指向
指针可以重新赋值,指向另一个同类型变量:
int a = 10, b = 20;
int *p = &a; // p指向a
p = &b; // p改为指向b(此时*p是20)
3. 空指针(nullptr
)
未指向任何有效变量的指针应赋值为nullptr
(C++11引入),表示“空指针”(不指向任何内存):
int *p = nullptr; // 空指针,不指向任何变量
// *p = 10; // 错误:解引用空指针会导致程序崩溃
注意:避免使用未初始化的“野指针”(未赋值的指针),其指向随机地址,操作会导致不可预知的错误:
int *p; // 野指针,未初始化,指向随机地址
// *p = 10; // 危险:可能修改任意内存,导致程序崩溃
五、指针与数组(信奥赛核心)
指针与数组(信奥赛核心)
数组名本质是指向数组第一个元素的指针,这是指针与数组关联的核心。
1. 数组名作为指针
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于 p = &arr[0](p指向数组第一个元素)
2. 通过指针访问数组元素
指针的算术运算(+
、-
)表示移动到下一个/上一个同类型元素:
p + i
指向arr[i]
(即数组第i
个元素)。*(p + i)
等价于arr[i]
(访问第i
个元素)。
示例:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
cout << *(p) << endl; // 1(arr[0])
cout << *(p + 1) << endl; // 2(arr[1])
cout << *(p + 3) << endl; // 4(arr[3])
// 遍历数组
for (int i = 0; i < 5; i++) {
cout << *(p + i) << " "; // 输出1 2 3 4 5
}
3. 指针遍历数组(更高效)
通过指针自增(p++
)移动到下一个元素,避免每次计算p + i
:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for (int i = 0; i < 5; i++) {
cout << *p << " "; // 输出当前元素
p++; // 指针移动到下一个元素
}
六、指针作为函数参数(传地址)
指针作为函数参数(传地址)
指针作为函数参数时,函数接收的是变量的地址,通过解引用可以直接修改实参的值(类似传引用,但语法不同)。
1. 实现两数交换(对比传值)
// 传值:无法交换实参
void swapByValue(int a, int b) {
int temp = a;
a = b;
b = temp;
}
// 传地址:通过指针修改实参
void swapByPointer(int *a, int *b) {
int temp = *a; // 解引用获取实参a的值
*a = *b; // 修改实参a的值
*b = temp; // 修改实参b的值
}
int main() {
int x = 3, y = 5;
swapByValue(x, y); // x和y不变
swapByPointer(&x, &y); // 传入x和y的地址,成功交换
cout << x << " " << y; // 输出5 3
return 0;
}
2. 函数返回多个结果(通过指针输出)
函数只能有一个返回值,通过指针参数可“输出”多个结果:
// 计算商和余数,通过指针输出余数
int divide(int a, int b, int *remainder) {
*remainder = a % b; // 余数通过指针输出
return a / b; // 商作为返回值
}
int main() {
int a = 10, b = 3, rem;
int quotient = divide(a, b, &rem); // 传入rem的地址
cout << "商:" << quotient << ",余数:" << rem; // 3,1
return 0;
}
七、const与指针(常指针与指针常量)
const与指针(常指针与指针常量)
const
与指针结合有两种常见形式,需注意区别:
1. 指向常量的指针(const 类型 *指针
)
指针指向的内容不能修改,但指针可以改变指向:
int a = 10, b = 20;
const int *p = &a; // p指向a,*p不可修改
// *p = 30; // 错误:不能修改指向的内容
p = &b; // 正确:可以改变指向
2. 指针常量(类型 *const 指针
)
指针本身不能改变指向,但指向的内容可以修改:
int a = 10, b = 20;
int *const p = &a; // p指向a,不能改指向
*p = 30; // 正确:可以修改指向的内容
// p = &b; // 错误:不能改变指向
八、信奥赛常见应用
信奥赛常见应用
1. 动态数组(未知大小的数组)
通过new
和delete
动态分配内存(信奥赛中处理输入大小不确定的场景):
int n;
cin >> n; // 输入数组大小(运行时确定)
int *arr = new int[n]; // 动态分配n个int的数组
// 使用数组
for (int i = 0; i < n; i++) {
cin >> arr[i];
}
// 释放内存(避免内存泄漏)
delete[] arr;
2. 处理二维数组(传递数组到函数)
二维数组作为函数参数时,需指定第二维大小,通过指针更灵活:
// 打印二维数组(3行4列)
void print2DArray(int (*arr)[4], int rows) { // arr是指向4个int的数组的指针
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
cout << arr[i][j] << " ";
}
cout << endl;
}
}
int main() {
int matrix[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
print2DArray(matrix, 3); // 传递二维数组名
return 0;
}
3. 链表与树(数据结构基础)
指针是实现链表、树等动态数据结构的核心(信奥赛进阶内容):
// 链表节点结构体
struct Node {
int data; // 数据
Node *next; // 指向 next 节点的指针
};
// 创建新节点
Node* createNode(int val) {
Node *newNode = new Node; // 动态分配节点
newNode->data = val;
newNode->next = nullptr;
return newNode;
}
九、避坑指南
避坑指南
解引用空指针/野指针:
这是最常见的错误,会导致程序崩溃。始终确保指针指向有效内存后再解引用:c++int *p = nullptr; if (p != nullptr) { // 先判断指针是否有效 *p = 10; }
指针越界访问:
访问数组时,指针不能超出数组范围(如*(p + n)
当n
大于数组长度时):c++int arr[5] = {1,2,3,4,5}; int *p = arr; // *(p + 10) = 100; // 错误:越界访问,可能修改其他内存
内存泄漏:
动态分配的内存(new
)必须用delete
释放,否则会耗尽内存:c++int *p = new int; // ... 使用p ... delete p; // 释放内存(单个变量用delete) p = nullptr; // 避免野指针 int *arr = new int[5]; delete[] arr; // 数组用delete[]
指针类型不匹配:
避免将一种类型的指针强制转换为另一种类型(除非明确知道后果):c++double d = 3.14; int *p = (int*)&d; // 危险:类型不匹配,*p的值不确定
十、实战:用指针反转数组
用指针反转数组
题目:通过指针操作,将数组元素反转(如[1,2,3,4,5]
→[5,4,3,2,1]
)。
#include <iostream>
using namespace std;
// 用指针反转数组
void reverseArray(int *arr, int n) {
int *left = arr; // 指向数组首元素
int *right = arr + n - 1; // 指向数组尾元素
while (left < right) {
// 交换left和right指向的元素
int temp = *left;
*left = *right;
*right = temp;
left++; // 左指针右移
right--; // 右指针左移
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int n = 5;
reverseArray(arr, n);
// 输出反转后的数组
for (int i = 0; i < n; i++) {
cout << arr[i] << " "; // 输出5 4 3 2 1
}
return 0;
}
重要
- 指针是C++中强大而灵活的工具,掌握指针能让你更深入地理解内存操作,尤其在信奥赛中处理动态数据、数组和复杂数据结构时不可或缺。- 核心是理解“指针存储地址,解引用访问值”的逻辑,同时警惕空指针、野指针和内存泄漏等常见错误。通过大量练习指针与数组、函数的结合使用,能显著提升代码的效率和灵活性。
基于指针的数组访问
基于指针的数组访问
在C++中,数组与指针存在紧密联系——数组名本质上是指向数组第一个元素的常量指针。利用这一特性,我们可以通过指针灵活访问数组元素,这在信奥赛中对优化数组操作、处理动态内存等场景非常重要。
一、数组名与指针的关系
数组名与指针的关系
数组名在大多数情况下会被隐式转换为指向数组首元素的指针(即&arr[0]
),这是指针访问数组的基础。
1. 数组名作为指针
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 等价于 int *p = &arr[0];(p指向数组第一个元素)
此时:
p
与arr
的值相同(都是数组首元素的地址)。*p
等价于arr[0]
(访问第一个元素)。
2. 指针的算术运算与数组访问
指针的算术运算(+
、-
)是以所指向元素的大小为单位的(而非字节)。对于int
指针,p + 1
表示移动到下一个int
元素(通常4字节)。
核心公式:*(p + i)
等价于 arr[i]
(访问数组第i
个元素)。
示例:
#include <iostream>
using namespace std;
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p指向arr[0]
cout << *p << endl; // 10(arr[0])
cout << *(p + 1) << endl; // 20(arr[1])
cout << *(p + 3) << endl; // 40(arr[3])
// 遍历数组
for (int i = 0; i < 5; i++) {
cout << *(p + i) << " "; // 输出10 20 30 40 50
}
return 0;
}
二、通过指针遍历数组(高效方式)
通过指针遍历数组(高效方式)
相比指针自增(p++
)移动到下一个元素,比*(p + i)
更高效,是信奥赛中遍历数组的常用技巧。
1. 基本遍历
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 指向首元素
for (int i = 0; i < 5; i++) {
cout << *p << " "; // 输出当前元素
p++; // 指针移动到下一个元素
}
2. 首尾指针遍历(双向遍历)
int arr[5] = {1, 2, 3, 4, 5};
int *left = arr; // 指向首元素
int *right = arr + 4; // 指向向尾元素(arr[4])
while (left <= right) {
cout << *left << " " << *right << " ";
left++; // 右移
right--; // 左移
}
// 输出:1 5 2 4 3 3
三、指针与多维数组
指针与多维数组
多维数组的数组名是指向第一行(或第一个维数组)的指针,通过指针访问需要注意维度的层级关系。
1. 二维数组与指针
对于二维维数组int arr[m][n]
中:
arr
是指向arr[0]
(第一行)的指针,类型为int (*)[n]
(指向含n
个int
的数组的指针)。arr[i]
是指向arr[i][0]
(第i
行首元素)的指针,类型为int*
。
访问方式:
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// 方式1:通过行指针访问
int (*rowPtr)[4] = matrix; // 行指针指向行的指针
cout << *(*rowPtr + 2) << endl; // 3(matrix[0][2])
cout << *(*(rowPtr + 1) + 1) << endl; // 6(matrix[1][1])
// 方式2:通过普通指针访问(按维为一维数组)
int *ptr = &matrix[0][0]; // 指向首个元素
cout << *(ptr + 5) << endl; // 6(第5个元素,matrix[1][1])
cout << *(ptr + 3 + 4) << endl; // 8(第3+4=7个元素,matrix[1][3])
2. 遍历二维数组
int matrix[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
int (*rowPtr)[4] = matrix;
// 遍历所有元素
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
cout << *(*(rowPtr + i) + j) << " "; // 等价于matrix[i][j]
}
cout << endl;
}
四、指针与动态数组(信奥赛核心)
指针与动态数组(信奥赛核心)
当数组大小未知(需运行时确定)时,需用new
动态分配内存,此时必须通过指针访问。
1. 动态一维数组
int n;
cin >> n; // 从输入获取数组大小
int *arr = new int[n]; // 动态分配n个int的数组(指针arr指向首元素)
// 赋值
for (int i = 0; i < n; i++) {
arr[i] = i * 10; // 等价于*(arr + i) = i * 10
}
// 输出
for (int i = 0; i < n; i++) {
cout << *(arr + i) << " "; // 等价于arr[i]
}
// 释放内存(必须做,否则内存泄漏)
delete[] arr;
arr = nullptr; // 避免野指针
2. 动态二维数组
动态二维数组本质是“指针的数组”(每个元素是指向一行的指针):
int rows, cols;
cin >> rows >> cols;
// 步骤1:分配行指针数组
int **matrix = new int*[rows];
// 步骤2:为每行分配列元素
for (int i = 0; i < rows; i++) {
matrix[i] = new int[cols]; // 每行是一个动态一维数组
}
// 赋值(通过指针访问)
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
*(matrix[i] + j) = i * cols + j; // 等价于matrix[i][j]
}
}
// 释放内存(先释放每行,再释放行指针)
for (int i = 0; i < rows; i++) {
delete[] matrix[i];
}
delete[] matrix;
matrix = nullptr;
五、指针访问数组的优势(信奥赛视角)
指针访问数组的优势(信奥赛视角)
- 灵活性:指针可随时改变指向,适合动态遍历(如双向遍历、跳跃访问)。
- 效率:指针算术运算通常比数组下标访问更快(尤其对大型数组)。
- 动态内存支持:动态数组只能通过指针访问,这是处理未知大小数据的必备能力。
- 函数传参:数组作为函数参数时,本质是传递指针,可避免整个数组的复制(节省内存)。
六、避坑指南
避坑指南
指针越界:
指针访问不能超出数组范围(如*(p + n)
当n
等于数组长度时),否则会访问非法内存:c++int arr[5] = {1,2,3,4,5}; int *p = arr; // *(p + 5) = 10; // 错误:越界访问(数组只有5个元素,下标0~4)
数组名不可修改:
数组名是“常量指针”,不能赋值或自增(区别于普通指针):c++int arr[5]; // arr++; // 错误:数组名是常量,不能修改指向 int *p = arr; p++; // 正确:普通指针可以修改
动态数组忘记释放:
动态分配的数组(new
)必须用delete[]
释放,否则会导致内存泄漏(信奥赛中程序可能因内存耗尽崩溃)。指针类型匹配:
指向二维数组行的指针必须指定列数,否则无法正确访问:c++int matrix[3][4]; int (*p)[4] = matrix; // 正确:指定列数为4 // int **p = matrix; // 错误:类型不匹配
七、实战:用指针实现数组去重
用指针实现数组去重
题目:删除有序数组中的重复元素,返回新长度(如[1,1,2,2,3]
→[1,2,3]
,长度3)。
#include <iostream>
using namespace std;
// 用指针去重有序数组,返回新长度
int removeDuplicates(int *arr, int n) {
if (n == 0) return 0;
int *slow = arr; // 慢指针:指向不重复元素的末尾
int *fast = arr + 1; // 快指针:遍历所有元素
while (fast < arr + n) {
if (*fast != *slow) { // 找到不重复元素
slow++; // 慢指针后移
*slow = *fast; // 覆盖重复元素
}
fast++; // 快指针继续后移
}
return slow - arr + 1; // 新长度 = 慢指针位置 - 首地址 + 1
}
int main() {
int arr[] = {1, 1, 2, 2, 3, 3, 3};
int n = 7;
int newLen = removeDuplicates(arr, n);
cout << "去重后长度:" << newLen << endl; // 3
cout << "去重后数组:";
for (int i = 0; i < newLen; i++) {
cout << arr[i] << " "; // 1 2 3
}
return 0;
}
重要
基于指针的数组访问是C++的核心技巧,尤其在信奥赛中处理动态内存、优化数组操作时不可或缺。核心是理解“数组名即指针”和“指针算术运算”的逻辑,通过指针的灵活性实现高效的遍历、修改和动态内存管理。同时需严格避免越界访问和内存泄漏,确保程序的正确性和稳定性。
字符指针
字符指针
在C++中,字符指针(char*
) 是指向字符类型的指针,因其与字符串的紧密关联而成为处理字符串的重要工具。字符指针既能指向单个字符,也能指向以'\0'
结尾的字符数组(C风格字符串),在信奥赛中常用于字符串操作、动态字符串处理等场景。
一、字符指针与字符串的关系
字符指针与字符串的关系
C风格字符串本质是以'\0'
结尾的字符数组,而字符指针可以指向这个数组的首地址,从而间接访问整个字符串。
1. 字符指针指向字符串常量
字符串常量(如"Hello"
)在内存中以字符数组形式存储(自动添加'\0'
),字符指针可直接指向其首地址:
#include <iostream>
using namespace std;
int main() {
const char *str = "Hello"; // str指向字符串首字符'H'
// 等价于:const char arr[] = "Hello"; const char *str = arr;
cout << str << endl; // 输出"Hello"(cout会自动打印到'\0')
cout << *str << endl; // 输出'H'(解引用获取首字符)
cout << *(str + 1) << endl; // 输出'e'(访问第二个字符)
return 0;
}
注意:字符串常量是只读的,因此指针通常声明为const char*
,避免误修改导致程序崩溃。
2. 字符指针指向字符数组
字符指针可指向手动定义的字符数组(字符串),此时可通过指针修改数组内容:
char arr[] = "World"; // 字符数组(可修改)
char *p = arr; // p指向数组首字符'W'
*p = 'w'; // 修改首字符为小写
cout << p << endl; // 输出"world"
*(p + 2) = 'R'; // 修改第三个字符
cout << p << endl; // 输出"woRld"
二、字符指针的运算与遍历
字符指针的运算与遍历
与其他指针类似,字符指针的算术运算以char
大小(1字节)为单位,非常适合遍历字符串。
1. 遍历字符串(方式1:下标法)
const char *str = "Hello";
for (int i = 0; str[i] != '\0'; i++) { // 以'\0'为结束标志
cout << str[i];
}
// 输出:Hello
2. 遍历字符串(方式2:指针自增)
const char *str = "Hello";
const char *p = str;
while (*p != '\0') { // 指针未指向结束符时循环
cout << *p;
p++; // 指针移动到下一个字符
}
// 输出:Hello
3. 计算字符串长度(模拟strlen
)
int myStrlen(const char *str) {
int len = 0;
while (*str != '\0') { // 遍历到'\0'
len++;
str++; // 指针后移
}
return len;
}
// 调用:
const char *s = "Hello";
cout << myStrlen(s); // 输出5
三、字符指针与字符串函数(<cstring>
)
C标准库提供了丰富的字符串函数,这些函数大多以字符指针作为参数,例如:
函数 | 功能 | 示例(str1="abc", str2="def" ) |
---|---|---|
strlen(p) | 计算字符串长度(不含'\0' ) | strlen("abc") → 3 |
strcpy(dst, src) | 复制字符串(src →dst ) | strcpy(dst, "abc") → dst 为"abc" |
strcat(dst, src) | 拼接字符串(src 追加到dst ) | strcat(dst, "def") → dst 为"abcdef" |
strcmp(p1, p2) | 比较字符串(字典序) | strcmp("abc", "abd") → -1 |
字符指针与字符串函数(<cstring>
)
示例:使用字符指针调用字符串函数
#include <cstring>
int main() {
char dst[20] = "Hello";
const char *src = " World";
// 拼接字符串
strcat(dst, src);
cout << dst << endl; // 输出"Hello World"
// 比较字符串
if (strcmp(dst, "Hello World") == 0) {
cout << "相等" << endl;
}
return 0;
}
四、动态字符串(字符指针与new
)
动态字符串(字符指针与new
)
字符指针结合new
可创建动态字符串(长度在运行时确定),这是信奥赛中处理输入长度不确定的字符串的常用方式。
1. 动态分配字符串
int n;
cout << "输入字符串长度:";
cin >> n;
cin.ignore(); // 清除换行符
// 分配n+1个字符(预留'\0'位置)
char *str = new char[n + 1];
cout << "输入字符串:";
cin.getline(str, n + 1); // 读取字符串
cout << str << endl; // 输出字符串
delete[] str; // 释放内存
str = nullptr;
2. 动态字符串的复制
// 复制源字符串到新的动态内存
char* copyString(const char *src) {
int len = strlen(src);
char *dst = new char[len + 1]; // 分配空间
strcpy(dst, src); // 复制内容
return dst;
}
// 调用:
const char *src = "Hello";
char *dst = copyString(src);
cout << dst << endl; // 输出"Hello"
delete[] dst;
五、字符指针与string
类的转换
字符指针与string
类的转换
在C++中,string
类与字符指针可相互转换,兼顾易用性和兼容性。
1. string
→ 字符指针(c_str()
)
string
类的c_str()
方法返回指向其内部字符数组的const char*
指针:
#include <string>
string s = "Hello";
const char *p = s.c_str(); // 转换为字符指针
cout << p << endl; // 输出"Hello"
2. 字符指针 → string
(直接赋值)
字符指针可直接赋值给string
对象:
const char *p = "World";
string s = p; // 转换为string
cout << s << endl; // 输出"World"
六、信奥赛常见应用
信奥赛常见应用
1. 字符串反转
void reverseString(char *str) {
if (str == nullptr) return; // 处理空指针
char *left = str;
char *right = str + strlen(str) - 1; // 指向最后一个有效字符
while (left < right) {
// 交换字符
char temp = *left;
*left = *right;
*right = temp;
left++;
right--;
}
}
// 调用:
char arr[] = "Hello";
reverseString(arr);
cout << arr; // 输出"olleH"
2. 字符串比较(忽略大小写)
#include <cctype> // 含tolower()
int strcmpIgnoreCase(const char *s1, const char *s2) {
while (*s1 != '\0' && *s2 != '\0') {
char c1 = tolower(*s1); // 转为小写
char c2 = tolower(*s2);
if (c1 != c2) {
return c1 - c2;
}
s1++;
s2++;
}
return *s1 - *s2; // 处理长度不同的情况
}
3. 统计子串出现次数
int countSubstring(const char *str, const char *sub) {
int count = 0;
int subLen = strlen(sub);
int strLen = strlen(str);
if (subLen == 0 || subLen > strLen) return 0;
for (int i = 0; i <= strLen - subLen; i++) {
// 检查从i开始的子串是否与sub匹配
bool match = true;
for (int j = 0; j < subLen; j++) {
if (str[i + j] != sub[j]) {
match = false;
break;
}
}
if (match) count++;
}
return count;
}
七、避坑指南
避坑指南
修改字符串常量:
字符串常量(如"Hello"
)是只读的,用非const
指针指向并修改会导致程序崩溃:c++char *p = "Hello"; // 危险:应声明为const char* // *p = 'h'; // 错误:修改只读内存,程序崩溃
字符数组未初始化
'\0'
:
手动创建字符数组时,必须确保以'\0'
结尾,否则字符串函数会读取垃圾值:c++char arr[6]; for (int i = 0; i < 5; i++) { arr[i] = 'A' + i; } arr[5] = '\0'; // 必须添加结束符,否则strlen结果不确定
动态字符串忘记释放:
用new[]
分配的字符数组必须用delete[]
释放,否则导致内存泄漏:c++char *str = new char[100]; // ... 使用 ... delete[] str; // 不可遗漏 str = nullptr; // 避免野指针
strcpy
的安全问题:strcpy
不会检查目标数组的容量,若源字符串过长会导致溢出,信奥赛中需确保目标容量足够:c++char dst[5]; const char *src = "HelloWorld"; // 长度10,超过dst容量 // strcpy(dst, src); // 错误:溢出,可能导致程序崩溃
八、实战:字符串替换
字符串替换
题目:将字符串中所有指定字符替换为新字符(如将"Hello"中的'l'替换为'x',结果为"Hexxo")。
#include <iostream>
#include <cstring>
using namespace std;
// 将str中所有oldChar替换为newChar
void replaceChar(char *str, char oldChar, char newChar) {
if (str == nullptr) return; // 空指针检查
while (*str != '\0') { // 遍历字符串
if (*str == oldChar) {
*str = newChar; // 替换字符
}
str++; // 指针后移
}
}
int main() {
char str[100];
cout << "输入字符串:";
cin.getline(str, 100);
char oldC, newC;
cout << "输入要替换的字符:";
cin >> oldC;
cout << "输入新字符:";
cin >> newC;
replaceChar(str, oldC, newC);
cout << "替换后:" << str << endl;
return 0;
}
重要
字符指针是处理C风格字符串的核心工具,在信奥赛中常用于需要直接操作内存的场景(如动态字符串、字符串算法优化)。掌握字符指针与字符串的关系、指针运算及常用字符串函数,能高效解决字符串反转、比较、替换等问题。同时需注意字符串结束符'\0'
的作用和动态内存的释放,避免常见错误。
指向结构体的指针
指向结构体的指针
在C++中,指向结构体的指针是存储结构体变量内存地址的指针变量。通过这种指针,我们可以间接访问和修改结构体的成员,在信奥赛中常用于动态创建结构体对象、处理结构体数组或实现链表等数据结构,能显著提升代码的灵活性和效率。
一、指向结构体的指针的定义与初始化
指向结构体的指针的定义与初始化
1. 定义语法
指向结构体的指针需要指定结构体类型,语法为:
struct 结构体名 *指针名;
2. 初始化(指向结构体变量)
通过取地址运算符(&) 获取结构体变量的地址,赋值给指针:
#include <iostream>
#include <string>
using namespace std;
// 定义结构体
struct Student {
string name;
int age;
double score;
};
int main() {
Student s = {"Alice", 15, 92.5}; // 结构体变量
Student *p = &s; // 指针p指向s(存储s的地址)
return 0;
}
二、通过指针访问结构体成员
通过指向结构体的指针访问结构体成员
通过指向结构体的指针访问成员有两种方式:
1. 解引用 + 点运算符((*p).成员
)
先解引用指针得到结构体变量,再用点运算符访问成员:
Student s = {"Alice", 15, 92.5};
Student *p = &s;
cout << (*p).name << endl; // 输出Alice
cout << (*p).age << endl; // 输出15
(*p).score = 95.0; // 修改score为95.0
2. 箭头运算符(p->成员
,推荐)
C++为结构体指针提供了更简洁的箭头运算符(->),直接通过指针访问成员:
Student s = {"Alice", 15, 92.5};
Student *p = &s;
cout << p->name << endl; // 输出Alice(等价于(*p).name)
cout << p->age << endl; // 输出15
p->score = 95.0; // 修改score为95.0(等价于(*p).score)
推荐使用箭头运算符,代码更简洁直观,是信奥赛中的常用写法。
三、指向结构体数组的指针
指向结构体数组的指针
结构体数组的数组名是指向第一个结构体元素的指针,通过指针遍历数组非常高效。
1. 基本用法
Student class1[3] = {
{"Alice", 15, 92.5},
{"Bob", 16, 88.0},
{"Charlie", 15, 95.5}
};
Student *p = class1; // p指向数组第一个元素(class1[0])
// 访问第一个元素的成员
cout << p->name << endl; // Alice
// 访问第二个元素的成员(指针+1指向class1[1])
cout << (p + 1)->age << endl; // 16
2. 遍历结构体数组
Student class1[3] = {
{"Alice", 15, 92.5},
{"Bob", 16, 88.0},
{"Charlie", 15, 95.5}
};
// 用指针遍历数组
for (Student *p = class1; p < class1 + 3; p++) {
cout << p->name << " " << p->age << " " << p->score << endl;
}
四、动态结构体(指针 + new)
动态结构体(指针 + new)
通过new
动态分配结构体对象,返回指向该对象的指针,这是信奥赛中处理动态数据的常用方式。
1. 动态创建单个结构体
// 动态分配一个Student对象
Student *p = new Student;
// 初始化成员(通过箭头运算符)
p->name = "David";
p->age = 17;
p->score = 85.0;
// 使用成员
cout << p->name << " " << p->score << endl;
// 释放内存(必须做,否则内存泄漏)
delete p;
p = nullptr; // 避免野指针
2. 动态创建结构体数组
int n;
cout << "输入学生数量:";
cin >> n;
// 动态分配n个Student的数组
Student *class2 = new Student[n];
// 赋值
for (int i = 0; i < n; i++) {
cout << "输入第" << i+1 << "名学生的姓名、年龄、成绩:";
cin >> class2[i].name >> class2[i].age >> class2[i].score;
}
// 输出
for (int i = 0; i < n; i++) {
cout << (class2 + i)->name << " " << (class2 + i)->score << endl;
}
// 释放数组内存
delete[] class2;
class2 = nullptr;
五、信奥赛核心应用:链表
信奥赛核心应用:链表
指向结构体的指针是实现链表的基础,链表中每个节点是一个结构体,通过指针指向向下一个节点。
// 定义链表节点结构体
struct Node {
int data; // 数据域
Node *next; // 指针域:指向 next 节点
};
// 创建新节点
Node* createNode(int val) {
Node *newNode = new Node; // 动态分配节点
newNode->data = val; // 设置数据
newNode->next = nullptr; // 初始指向空
return newNode;
}
// 在链表末尾插入节点
void append(Node(Node *&head, int val) { // head是指向指针的引用
Node *newNode = createNode(val);
if (head == nullptr) { // 链表为空时,新节点作为头
head = newNode;
return;
}
// 遍历到链表末尾
Node *p = head;
while (p->next != nullptr) {
p = p->next;
}
p->next = newNode; // 插入新节点
}
// 打印链表
void printList(Node *head) {
Node *p = head;
while (p != nullptr) {
cout << p->data << " ";
p = p->next; // 移动到下一个节点
}
cout << endl;
}
// 释放链表内存
void freeList(Node *&head) {
while (head != nullptr) {
Node *temp = head;
head = head->next; // 移动到下一个节点
delete temp; // 释放当前节点
}
}
// 使用示例
int main() {
Node *head = nullptr; // 链表头指针
appendNode(head, 10);
appendNode(head, 20);
appendNode(head, 30);
printList(head); // 输出10 20 30
freeList(head); // 释放内存
return 0;
}
六、避坑指南
避坑指南
空指针访问成员:
指针未指向有效结构体对象时(如nullptr
),访问成员会导致程序崩溃:c++Student *p = nullptr; // cout << p->name; // 错误:解引用空指针
忘记释放动态结构体:
用new
创建的结构体/数组必须用delete
/delete[]
释放,否则内存泄漏:c++Student *p = new Student; // ... 使用 ... delete p; // 单个对象用delete Student *arr = new Student[5]; delete[] arr; // 数组用delete[]
指针算术运算越界:
指向结构体数组的指针,p + i
不能超出数组范围:c++Student arr[3]; Student *p = arr; // (p + 5)->age = 10; // 错误:越界访问
结构体指针的类型匹配:
指针类型必须与结构体类型严格匹配:c++struct A { int x; }; struct B { int y; }; A a; B *p = &a; // 错误:类型不匹配
七、实战:结构体指针排序
结构体指针排序
题目:用指针数组存储结构体指针,按成绩排序后输出学生信息。
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
struct Student {
string name;
int age;
double score;
};
// 比较函数:按成绩降序
bool compare(Student *a, Student *b) {
return a->score > b->score;
}
int main() {
// 创建3个学生对象
Student s1 = {"Alice", 15, 92.5};
Student s2 = {"Bob", 16, 88.0};
Student s3 = {"Charlie", 15, 95.5};
// 定义结构体指针数组(存储指向学生的指针)
Student *arr[] = {&s1, &s2, &s3};
int n = 3;
// 排序指针数组(不移动结构体,只交换指针)
sort(arr, arr + n, compare);
// 输出排序结果
for (int i = 0; i < n; i++) {
cout << arr[i]->name << " " << arr[i]->score << endl;
}
// 输出:Charlie 95.5 → Alice 92.5 → Bob 88.0
return 0;
}
重要
指向结构体的指针是处理复杂数据的强大工具,尤其在信奥赛中实现动态数据结构(如链表)、优化结构体数组操作时不可或缺。核心是掌握箭头运算符(->
)的使用,以及动态内存管理(new
/delete
)。通过指针操作结构体,既能节省内存(避免复制大对象),又能提高代码灵活性,是进阶编程的必备技能。
引用
引用的基本定义与特性
在C++中,引用(Reference) 是变量的“别名”,它与指针类似但语法更简洁,用于间接访问变量。引用一旦绑定到某个变量,就会一直指向该变量,不能再改变指向。在信奥赛中,引用常用于函数参数传递和返回值,既能避免值传递的内存开销,又比指针更安全易用。
一、引用的基本定义与特性
引用的定义与特性
1. 定义语法
引用的定义需要在变量类型后加&
,并必须在声明时初始化(绑定到一个已存在的变量):
数据类型 &引用名 = 变量名;
示例:
#include <iostream>
using namespace std;
int main() {
int a = 10;
int &b = a; // b是a的引用(别名)
cout << "a = " << a << endl; // 10
cout << "b = " << b << endl; // 10(b与a的值相同)
b = 20; // 修改b,等价于修改a
cout << "a = " << a << endl; // 20(a的值被改变)
cout << "&a = " << &a << endl; // 输出a的地址
cout << "&b = " << &b << endl; // 输出与&a相同的地址(引用与原变量同内存)
return 0;
}
2. 核心特性
- 必须初始化:引用声明时必须绑定到一个变量,不能像指针一样先声明后赋值。
int &b; // 错误:引用必须初始化
- 不可更改指向:一旦绑定到某个变量,引用就不能再指向其他变量。
int a = 10, c = 20;
int &b = a;
b = c; // 这是将c的值赋给a(而非改变引用指向)
- 无空引用:引用必须指向有效变量,不存在“空引用”(区别于空指针
nullptr
)。 - sizeof引用:
sizeof(引用)
等于所指向变量的大小(而非地址大小)。
二、引用作为函数参数(传引用)
引用作为函数参数
引用最常用的场景是作为函数参数,实现“传引用调用”,此时函数内部对引用的修改会直接影响实参。
1. 实现变量交换(对比传值)
// 传值:无法交换实参(操作副本)
void swapByValue(int x, int y) {
int temp = x;
x = y;
y = temp;
}
// 传引用:直接操作实参(x是a的别名,y是b的别名)
void swapByRef(int &x, int &y) {
int temp = x;
x = y;
y = temp;
}
int main() {
int a = 3, b = 5;
swapByValue(a, b); // a和b不变
swapByRef(a, b); // a和b成功交换
cout << a << " " << b; // 输出5 3
return 0;
}
2. 高效传递大对象
对于结构体、数组等大对象,传值会复制整个对象(开销大),传引用无需复制,效率更高:
struct Student {
string name;
int age;
// ... 其他成员
};
// 传引用访问学生信息(不复制,效率高)
void printStudent(const Student &s) { // const确保不修改实参
cout << s.name << " " << s.age << endl;
}
const
引用:用于只读场景,防止函数内部意外修改实参,同时允许传递常量或表达式:
void func(const int &x) {
// x = 10; // 错误:const引用不可修改
}
// 调用
int a = 5;
func(a); // 正确
func(10); // 正确(const引用可绑定常量)
func(a + 3); // 正确(可绑定表达式)
3. 函数返回多个结果
函数只能有一个返回值,通过引用参数可“输出”多个结果(替代指针的常用方式):
// 计算商和余数,通过引用参数输出余数
int divide(int a, int b, int &remainder) {
remainder = a % b; // 余数通过引用输出
return a / b; // 商作为返回值
}
int main() {
int a = 10, b = 3, rem;
int quotient = divide(a, b, rem);
cout << "商:" << quotient << ",余数:" << rem; // 3 1
return 0;
}
三、引用作为函数返回值
引用作为函数返回值
函数可以返回引用,此时返回的是变量的别名,可用于赋值或连续操作。
1. 基本用法
int &getElement(int arr[], int index) {
return arr[index]; // 返回数组元素的引用
}
int main() {
int arr[] = {10, 20, 30};
getElement(arr, 1) = 50; // 通过返回的引用修改arr[1]
cout << arr[1]; // 输出50
return 0;
}
2. 注意事项
- 不能返回局部变量的引用:局部变量在函数结束后销毁,引用会变成“悬垂引用”(指向无效内存)。
int &badFunc() {
int x = 10;
return x; // 错误:返回局部变量的引用
}
- 通常返回类成员或全局变量的引用(生命周期长于函数调用)。
四、引用与指针的区别(核心对比)
特性 | 引用(int & ) | 指针(int * ) |
---|---|---|
初始化 | 必须在声明时初始化(绑定变量) | 可先声明后赋值(可指向nullptr ) |
指向修改 | 一旦绑定,不能改变指向 | 可随时改变指向其他变量 |
空值 | 无空引用(必须指向有效变量) | 有空指针(nullptr ) |
语法 | 直接使用(b = 20 ) | 需要解引用(*p = 20 ) |
内存开销 | 无额外开销(只是别名) | 存储地址,有内存开销(通常4/8字节) |
安全性 | 更高(无空引用、悬垂引用风险较低) | 较低(可能解引用空指针、野指针) |
五、信奥赛常见应用
信奥赛常见应用
1. 优化递归/循环中的参数传递
对于大型数组或结构体,传引用可避免重复复制,显著提升效率:
// 递归计算数组和(传引用避免数组复制)
int sumArray(const int arr[], int n) { // 数组参数本质是指针,但const仍有效
if (n == 0) return 0;
return arr[n-1] + sumArray(arr, n-1);
}
2. 简化结构体/类的操作
通过引用直接修改结构体成员,代码更简洁:
struct Point {
int x, y;
};
// 移动点的坐标(传引用直接修改)
void movePoint(Point &p, int dx, int dy) {
p.x += dx;
p.y += dy;
}
3. 实现链式操作
返回引用允许连续赋值,如a = b = c
的逻辑:
class Counter {
private:
int val;
public:
Counter(int v = 0) : val(v) {}
Counter& increment() { // 返回自身引用
val++;
return *this;
}
int getVal() { return val; }
};
// 调用:
Counter c(5);
c.increment().increment(); // 连续调用
cout << c.getVal(); // 7
六、避坑指南
避坑指南
引用必须初始化:
忘记初始化引用会导致编译错误:c++int &ref; // 错误:引用声明时必须初始化
避免返回局部变量的引用:
局部变量在函数返回后销毁,引用会指向无效内存,导致程序崩溃:c++string &badReturn() { string s = "hello"; return s; // 危险:s会被销毁 }
const
引用的只读性:
试图修改const
引用会导致编译错误,这是一种保护机制:c++const int &ref = 10; // ref = 20; // 错误:const引用不可修改
引用不是变量:
引用没有自己的内存空间,只是原变量的别名,不能对引用取地址再赋值给指针:c++int a = 10; int &b = a; int *p = &b; // 正确:p指向a(&b等价于&a)
七、实战:用引用排序结构体数组
用引用排序结构体数组
题目:定义学生结构体,包含姓名和成绩,用引用传递比较函数,按成绩降序排序。
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
struct Student {
string name;
double score;
};
// 比较函数:按成绩降序(参数用const引用,避免复制)
bool compare(const Student &a, const Student &b) {
return a.score > b.score;
}
int main() {
Student arr[] = {
{"Alice", 92.5},
{"Bob", 88.0},
{"Charlie", 95.5}
};
int n = 3;
sort(arr, arr + n, compare); // 排序
for (int i = 0; i < n; i++) {
cout << arr[i].name << " " << arr[i].score << endl;
}
// 输出:
// Charlie 95.5
// Alice 92.5
// Bob 88.0
return 0;
}
重要
引用是C++中简化指针操作的重要特性,核心优势是语法简洁和安全性高。在信奥赛中,传引用参数是替代指针的首选方式,尤其适合需要修改实参、传递大对象或实现多返回值的场景。掌握引用与指针的区别,合理使用const
引用,能写出更高效、清晰且安全的代码。