指针
基本概念
在C++
中,指针是存储其他变量的内存地址的变量。
我们在程序中声明的每个变量在内存中都有一个关联的位置,我们称之为变量的内存地址。
如果我们的程序中有一个变量 var
,那么&var
返回它的内存地址。
int main()
{
int var1 = 3;
int var2 = 90;
int var3 = 520;
cout<<"Address of var1: "<<&var1<<endl;
cout<<"Address of var2: " <<&var2<<endl;
cout<<"Address of var3: "<<&var3 <<endl;
return 0;
}
- 开头的0x代表十六进制形式的地址。
- 注意,第一个地址与第二个地址相差 4 个字节,第二个地址与第三个地址相差 4 个字节。
- 区别是因为在64位系统中int的大小是4个字节
- 运行该程序时,您可能不会得到相同的结果。这是因为地址取决于程序运行的环境。
如何定义指针:
int* pointer;
以下是我们如何为指针分配地址:
int var = 5;
int* ptr = &var;
在这里,变量 var
被赋值为 5。然后,通过代码 ptr = &var,
将变量 var
的地址分配给 ptr
指针。
声明指针之后立即对其进行赋值操作是一个不错的编码习惯;
- 通过指针获取值
需要获取指针指向的变量地址中的值,可以使用*
来进行;
int main()
{
int var1 = 5;
int* ptr = &var1;
cout<<"Address of var1: "<<&var1<<endl;
cout<<"ptr value: "<< *ptr<<endl;
return 0;
}
我们使用 *point_var
来获取存储在该地址中的值。当将 *
与指针一起使用时,称之为解引用运算符。它作用于指针并给出指针中存储的地址指向的值。也就是说,*point_var = var
。
再看一个例子,强化对指针和解引用概念的理解:
#include <iostream>
using namespace std;
int main() {
int var = 5;
// store address of var
int* point_var = &var;
// print value of var
cout << "var = " << var << endl;
// print address of var
cout << "Address of var (&var) = " << &var << endl
<< endl;
// print pointer point_var
cout << "point_var = " << point_var << endl;
// print the content of the address point_var points to
cout << "Content of the address pointed to by point_var (*point_var) = " << *point_var << endl;
return 0;
}
- 改变指针指向的值
如果point_var
指向var
的地址,我们可以使用*point_var
来改变var
的值。
int main()
{
int var1 = 3;
int* ptr = &var1;
*ptr = 1;
cout<<"Address of var1: "<<&var1<<endl;
cout<<"ptr value: "<< *ptr<<endl;
return 0;
}
这里,
point_var
和&var
具有相同的地址;当*point_var
改变时,var
的值也会改变。
- 使用指针时候的常见错误用法:
int var = 5;
// Wrong!
// point_var is an address but var is not
int* point_var = var;
// Wrong!
// &var is an address
// *point_var is the value stored in &var
*point_var = &var;
// Correct!
// point_var is an address and so is &var
point_var = &var;
// Correct!
// both *point_var and var are values
*point_var = var;
指针和数组
在 C++
中,指针是保存其他变量地址的变量。指针不仅可以存储单个变量的地址,还可以存储数组单元的地址。
int *ptr;
int arr[5];
// store the address of the first
// element of arr in ptr
ptr = arr;
- 这里,
ptr
是一个指针变量,而arr
是一个int
数组。- 代码
ptr = arr
; 将数组的第一个元素的地址存储在变量ptr
中。- 注意,我们使用的是arr而不是&arr[0]。这是因为它们是相同的。因此,下面的代码与上面的代码相同。
int *ptr;
int arr[5];
ptr = &arr[0];
其余数组元素的地址由 &arr[1]、&arr[2]、&arr[3]
和 &arr[4]
给出。
- 使用指针获取指定元素
假设我们需要使用相同的指针 ptr
指向数组的第四个元素。
这里,如果 ptr
指向上例中的第一个元素,那么 ptr + 3
将指向第四个元素地址。例如:
int *ptr;
int arr[5];
ptr = arr;
ptr + 1 is equivalent to &arr[1];
ptr + 2 is equivalent to &arr[2];
ptr + 3 is equivalent to &arr[3];
ptr + 4 is equivalent to &arr[4];
类似地,我们可以使用单个指针访问元素。例如:
*ptr == arr[0];
*(ptr + 1) is equivalent to arr[1];
*(ptr + 2) is equivalent to arr[2];
*(ptr + 3) is equivalent to arr[3];
*(ptr + 4) is equivalent to arr[4];
假设我们已经初始化了 ptr = &arr[2];
ptr - 2 is equivalent to &arr[0];
ptr - 1 is equivalent to &arr[1];
ptr + 1 is equivalent to &arr[3];
ptr + 2 is equivalent to &arr[4];
int main()
{
float arr[3];
float *ptr;
for(int i=0;i<3;i++)
{
cout<<"&arr["<<i<<"]=" <<&arr[i] <<endl;
}
ptr = arr;
cout<<endl;
for(int i=0;i<3;i++)
{
cout <<"ptr + " << i <<" = " << ptr+i<<endl;
}
return 0;
}
在上面的程序中,我们分别使用普通方式和指针的方式打印了数组元素的地址信息;
在大多数情况下,数组名称会退化为指针。简单来说,数组名称被转换为指针。这就是为什么我们可以使用指针来访问数组元素的原因。但是,我们应该记住,指针和数组并不相同。在某些情况下,数组名称不会衰减为指针。具体情况可以查阅:https://stackoverflow.com/questions/17752978/exceptions-to-array-decaying-into-a-pointer
// C++ Program to insert and display data entered by using pointer notation.
#include <iostream>
using namespace std;
int main() {
float arr[5];
// Insert data using pointer notation
cout << "Enter 5 numbers: ";
for (int i = 0; i < 5; ++i) {
// store input number in arr[i]
cin >> *(arr + i) ;
}
// Display data using pointer notation
cout << "Displaying data: " << endl;
for (int i = 0; i < 5; ++i) {
// display value of arr[i]
cout << *(arr + i) << endl ;
}
return 0;
}
其中的cin >> *(arr + i) ;
等同于cin >> arr[i];
;
请注意,我们没有声明单独的指针变量,而是使用数组名称 arr 作为指针表示法。
我们已经知道,数组名 arr 指向数组的第一个元素。因此,我们可以将 arr 视为一个指针。
引用
基本概念
我们使用&符号来创建引用。例如:
string& ref_city = city;
#include <iostream>
using namespace std;
int main() {
string city = "Paris";
// create a reference to the variable
string& ref_city = city;
// display the variable
cout << "Variable Value: " << city << endl;
cout << "Reference Value: " << ref_city << endl;
return 0;
}
上面的代码中我们使用引用变量 ref_city
来显示变量 city
的值。
- 通过引用修改变量
我们可以通过简单地为引用变量分配一个新值来修改变量。
int main()
{
string city = "Shanghai";
string& ref_city = city;
ref_city = "Hk";
cout <<"var:" << city <<endl;
cout<<"ref: "<< ref_city<<endl;
return 0;
}
我们可以在创建引用时将 &
符号与数据类型或变量一起放置。但是,标准做法是将符号与数据类型一起使用。例如:
// create a variable
string city = "Paris";
// valid but not a standard practice
string &ref_city = city;
// valid and a standard practice
sring& ref_city = city;
一旦我们创建了对变量的引用,就无法将其更改为引用另一个变量。例如:
int main() {
string city1 = "Paris";
// create a reference to the variable
string& ref_city = city1;
// display the variable
cout << "city1 = " << city1 << endl;
cout << "ref_city = " << ref_city << endl;
string city2 = "New York";
// trying to modify the ref_city reference variable to refer to city2
// but it assigns the value of city2 to the variable city1
ref_city = city2;
// display the variables
cout << endl << "city1 = " << city1 << endl;
cout << "city2 = " << city2 << endl;
cout << "ref_city = " << ref_city << endl;
return 0;
}
注意,虽然这里最后引用
ref_city
输出的是新的值New York
,但他指向的始终还是开始的那个city1
,只是引用的值变为了新的New York
;
引用传递
在 C++ 函数教程中,我们学习了如何将参数传递给函数。使用的这种方法称为按值传递,因为传递的是实际值。
但是,还有另一种传递参数的方法,称为引用传递。
引用传递是一种在函数中传递参数的方法,其中将实际参数的引用而不是它们的值传递给函数。
// function that takes value as parameter
void func1(int num_val) {
// code
}
// function that takes reference as parameter
// notice the & before the parameter
void func2(int& num_ref) {
// code
}
int main() {
int num = 5;
// pass by value
func1(num);
// pass by reference
func2(num);
return 0;
}
注意 void func2(int& num_ref)
中的 &
。这表示我们使用变量的引用作为参数。
因此,当我们通过传递变量 num
作为参数来调用 main()
中的func2()
函数时,我们实际上传递的是 num
变量的引用而不是值 5
。
void swap(int &n1,int &n2)
{
int temp;
temp = n1;
n1 = n2;
n2 = temp;
}
int main() {
int a {1},b {2};
cout<<"Before swaping" << endl;
cout<<"a = "<< a <<endl;
cout<<"b = "<< b << endl;
swap(a,b);
cout<<"\nAfter swaping" << endl;
cout<<"a = "<< a <<endl;
cout<<"b = "<< b << endl;
return 0;
}
在这个程序中,我们将变量 a 和 b 传递给 swap()
函数。请注意函数的定义、其中的参数我们使用的是引用类型;
因此,编译器可以识别出传递给函数参数的不是实际值,而是变量的引用。
在 swap()
函数中,函数参数 n1
和 n2
分别指向与变量 a
和 b
相同的值。因此,交换是在实际值上进行的。
常量引用
当变量的值不需要更改时,我们可以将它们作为常量引用传递。
我们来看一个例子:
#include <iostream>
using namespace std;
// function to add two numbers
// using const references
int add(const int& num1, const int& num2) {
return num1 + num2;
}
int main() {
int number1, number2;
// take input
cout << "Enter the first number: ";
cin >> number1;
cout << "Enter the second number: ";
cin >> number2;
// call add function
int sum = add(number1, number2);
// displaying the result
cout << "The sum of " << number1 << " and " << number2 << " is " << sum << endl;
return 0;
}
在这里,我们使用 const 关键字通过 const 引用传递值。
使用 const
引用可防止函数内的值发生更改。例如,让我们尝试使用 const
引用来交换两个数字。
#include <iostream>
using namespace std;
// function definition to swap values
// using const references
void swap(const int& n1,const int& n2) {
int temp;
temp = n1;
n1 = n2;
n2 = temp;
}
int main() {
// initialize variables
int a = 1, b = 2;
cout << "Before swapping" << endl;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
// call function to swap numbers
swap(a, b);
cout << "\nAfter swapping" << endl;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
这个程序运行是会报错的,因为对于参数,我们使用了const
进行修饰
对于这个交换函数,我们使用指针传递参数也是可以实现的,但是不建议使用这种方式:
#include <iostream>
using namespace std;
// function prototype with pointers as parameters
void swap(int*, int*);
int main() {
// initialize variables
int a = 1, b = 2;
cout << "Before swapping" << endl;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
// call function by passing variable addresses
swap(&a, &b);
cout << "\nAfter swapping" << endl;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
return 0;
}
// function definition to swap numbers
void swap(int* n1, int* n2) {
int temp;
temp = *n1;
*n1 = *n2;
*n2 = temp;
}
*n1
和*n2
分别给出存储在地址 n1 和 n2 处的值。*- 由于
n1
和n2
包含a和b的地址,因此对*n1
和*n2
进行任何操作都会改变a
和b
的实际值。因此,当我们在main()
函数中打印a
和b
的值时,这些值会发生变化,也就完成了交换;
使用引用而不是指针通常更容易且不易出错,因为它不涉及直接的指针操作。
指针只能用于在特别需要指针的上下文中或与 C 库交互时传递参数。
内存管理
- C++ 允许我们在运行时动态分配内存。这就是所谓的动态内存分配。
- 在 Java 和 Python 等其他编程语言中,编译器会自动管理分配给变量的内存。但 C++ 并非如此。
- 在 C++ 中,当我们不再使用变量时,需要手动取消分配动态分配的内存。
- 我们可以使用 new 和 delete 操作符分别动态分配和取消分配内存。
new关键字
我们可以使用新表达式在运行时分配内存。例如
int* point_var;
point_var = new int;
*point_var = 45;
在这里,我们使用 new
表达式为一个 int
变量动态分配了内存。
请注意,我们使用了指针 point_var
来动态分配内存。这是因为 new
表达式返回的是内存位置的地址。
我们还可以在相同的步骤中分配内存并初始化值:
int* point_var = new int{45};
使用此语法可以避免未初始化的指针。取消引用时,未初始化的指针可能会导致未定义的行为。所以这是首选语法。
使用新表达式的语法是:
data_type* pointer_variable = new data_type{value};
delete关键字
一旦我们不再需要使用动态声明的变量,就可以取消分配该变量占用的内存。
为此,我们可以使用delete
关键字。它将内存归还给操作系统。这就是所谓的内存去分配。
删除表达式的语法如下:
int* point_var = new int{45};
// print the value stored in memory
cout << *point_var;
// deallocate the memory
delete point_var;
// set pointer to nullptr
point_var = nullptr;
在这里,我们使用指针 point_var
为一个 int
变量动态分配了内存。
打印完 point_var
的内容后,我们使用 delete
删除了内存。
一个好的做法是,在取消分配内存后将指针设置为 nullptr
,以避免在指针被取消引用时出现未定义的行为。
不正确地删除内存会造成内存泄漏,进而导致程序消耗大量内存。正确使用删除表达式对于防止内存泄漏和确保高效内存管理至关重要。
下面再看两个例子,加深理解:
- ex1
#include <iostream>
using namespace std;
int main() {
// dynamically allocate memory
int* point_int = new int{45};
float* point_float = new float{45.45f};
cout << *point_int << endl;
cout << *point_float << endl;
// deallocate the memory
// set pointers to nullptr
delete point_int;
delete point_float;
return 0;
}
在这个程序中,我们为两个 int 和 float 类型的变量动态分配了内存。在为它们赋值并打印后,我们最后使用删除表达式去分配内存。
动态内存分配可以提高内存管理的效率,尤其是对于数组而言,很多时候我们可能要到运行时才能知道数组的大小。
- ex2
#include <iostream>
using namespace std;
int main() {
int num;
cout << "Enter total number of students: ";
cin >> num;
float* ptr;
// memory allocation of num number of floats
ptr = new float[num];
cout << "Enter GPA of students." << endl;
for (int i = 0; i < num; ++i) {
cout << "Student" << i + 1 << ": ";
cin >> *(ptr + i);
}
cout << "\nDisplaying GPA of students." << endl;
for (int i = 0; i < num; ++i) {
cout << "Student" << i + 1 << ": " << *(ptr + i) << endl;
}
// ptr memory is released
delete[] ptr;
ptr = nullptr;
return 0;
}
在这个程序中,我们要求用户输入学生人数,并将其存储在 num 变量中。然后,我们使用 new 为浮点数组动态分配了内存。
我们使用指针符号将数据输入数组(随后打印数据)。在不再需要数组后,我们使用代码去分配数组内存:
注意删除后的[]。我们使用方括号 [] 来表示内存的删除是删除一个数组。
- ex3
#include <iostream>
using namespace std;
class Student {
private:
int age;
public:
// constructor initializes age to 12
Student() : age(12) {}
void get_age() {
cout << "Age = " << age << endl;
}
};
int main() {
// dynamically declare student object
Student* ptr = new Student();
// call get_age() function
ptr->get_age();
// ptr memory is released
delete ptr;
return 0;
}
在这个程序中,我们创建了一个 Student
类,该类有一个私有变量 age
。
我们在默认构造函数 Student()
中将 age 初始化为 12,并使用函数 get_age()
打印了它的值。
在 main()
中,我们使用 new
表达式创建了一个 Student
对象,并使用指针 ptr
指向其地址。
对象创建后,Student()
构造函数将年龄初始化为 12 岁。
为什么要使用动态内存分配?
动态内存分配有几个优点,例如:
- 灵活性: 动态内存分配允许我们在运行时根据需要分配内存。当编译时不知道数据结构的大小,或在程序执行过程中数据结构的大小发生变化时,这种灵活性就非常有用。
- 数据结构: 数据结构(如链表、树、图和可调整大小的数组(C++ 中的向量))通常需要动态分配内存,以容纳不同数量的数据。
- 资源管理: 我们可以在需要时分配内存,并在不再需要时取消分配。这样可以提高资源利用率。
- 动态数组: 在 C++ 等语言中,静态数组的大小是在编译时确定的。动态内存分配允许我们创建数组,其大小可在运行时确定。