本系列文章用于记录,近期温习C++过程中的一些笔记内容,本文主要记录指针相关的内容。
全部内容分为上下两篇
(1)一维数组
将一个整型变量加1后,其值将增加1。但是,将指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数。
①数组在内存中占用的空间是连续的。
②C++将数组名解释为数组第0个元素的地址。
③数组第0个元素的地址和数组首地址的取值是相同的。
④数组第n个元素的地址是:数组首地址+n
⑤C++编译器把 数组名[下标] 解释为 *(数组首地址+下标)
数组是占用连续空间的一块内存,在多数情况下,数组名被解释为数组第0个元素的地址。C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的。但是,将sizeof运算符用于数据名时,将返回整个数组占用内存空间的字节数。可以修改指针的值,但数组名是常量,不可修改。
示例12
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main()
{
int a[5] = { 6 , 66 , 666 , 6666 , 66666 };
//输出地址
cout << "a的值是:" << (long long)a << endl;
cout << "&a的值是:" << (long long)&a << endl;
cout << "a[0]的地址是:" << (long long)&a[0] << endl;
cout << "a[1]的地址是:" << (long long)&a[1] << endl;
int* p = a;
cout << "p的值是:" << (long long)p << endl;
cout << "p+0的值是:" << (long long)(p + 0) << endl;
cout << "p+1的值是:" << (long long)(p + 1) << endl;
//输出值
cout << "a[0]的值是:" << a[0] << endl;
cout << "a[1]的值是:" << a[1] << endl;
cout << "*(p+0)的值是:" << *(p + 0) << endl;
cout << "*(p+1)的值是:" << *(p + 1) << endl;
}
示例12输出结果
a的值是:557739014152
&a的值是:557739014152
a[0]的地址是:557739014152
a[1]的地址是:557739014156
p的值是:557739014152
p+0的值是:557739014152
p+1的值是:557739014156
a[0]的值是:6
a[1]的值是:66
*(p+0)的值是:6
*(p+1)的值是:66
一维数组用于函数的参数时,只能传数组的地址,并且必须把数组长度也传进去,除非数组中有最后一个元素的标志。
书写方法有两种:
void func(int* arr, int len);
void func(int arr[], int len);
在函数中,可以用数组表示法,也可以用指针表示法,不要对指针名用sizeof运算符,它不是数组名。
示例13
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// void func(int *arr,int len)
void func(int arr[],int len)
{
for (int ii = 0; ii < len; ii++)
{
cout << "arr[" << ii << "]的值是:" << arr[ii] << endl; // 用数组表示法操作指针。
cout << "*(arr+" << ii << ")的值是:" << *(arr + ii) << endl; // 地址[下标] 解释为 *(地址+下标)。
}
}
int main()
{
int a[] = {2,8,4,6,7,1,9};
func(a, sizeof(a) / sizeof(int));
}
示例13输出结果
arr[0]的值是:2
*(arr+0)的值是:2
arr[1]的值是:8
*(arr+1)的值是:8
arr[2]的值是:4
*(arr+2)的值是:4
arr[3]的值是:6
*(arr+3)的值是:6
arr[4]的值是:7
*(arr+4)的值是:7
arr[5]的值是:1
*(arr+5)的值是:1
arr[6]的值是:9
*(arr+6)的值是:9
声明行指针的语法:数据类型 (*行指针名)[行的大小]; // 行的大小即数组长度。
几个行指针的示例如下:
int (*p1)[3]; // p1是行指针,用于指向数组长度为3的int型数组。
int (*p2)[5]; // p2行指针,用于指向数组长度为5的int型数组。
double (*p3)[5]; // p3是行指针,用于指向数组长度为5的double型数组。
一维数组名被解释为数组第0个元素的地址,若对一维数组名+1,得到的是数组中下一个元素的地址;对一维数组名取地址得到的是数组的地址,是行地址,若对一维数组名取地址+1,得到的是下一个数组的地址,如对于int a[10],&a+1得到的是数组a的地址+40,如下所示:
示例14:
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main()
{
int a[10];
cout << "数组a第0个元素的地址:" <<(long long) a << endl;
cout << "数组a的地址:" << (long long)&a << endl;
cout << "数组a第0个元素的地址+1:" << (long long)(a + 1) << endl; // 地址的增加量是4。
cout << "数组a的地址+1:" << (long long)( & a + 1) << endl; // 地址的增加量是40。
}
示例14输出结果:
数组a第0个元素的地址:621223605816
数组a的地址:621223605816
数组a第0个元素的地址+1:621223605820
数组a的地址+1:621223605856
在上面的例子中,定义指针和行指针指向数组a的语法如下:
int* p1 = a; //指针
int(*p2)[10] = &a; //行指针
(2)多维数组
二维数组:int bh[2][3] = { {11,12,13},{21,22,23} };
二维数组名是行地址,bh是二维数组名,该数组有2两元素,每一个元素本身又是一个数组长度为3的整型数组,bh被解释为数组长度为3的整型数组类型的行地址。如果存放bh的值,要用数组长度为3的整型数组类型的行指针。
int (*p)[3]=bh;
注意:不能使用 int *p=bh ,会报错
三维数组: int bh[4][2][3];
bh是三维数组名,该数组有4元素,每一个元素本身又是一个2行3列的二维数组。bh被解释为2行3列的二维数组类型的二维地址。如果存放bh的值,要用2行3列的二维数组类型的二维指针。
int (*p)[2][3]=bh;
其他高维数组以此类推、、、、、、
接下来看一下如何把二维数组传递给函数,如果要把上面的二维数组bh传给函数,函数的声明如下:
void func(int (*p)[3],int len);
void func(int p[][3],int len);
示例15:
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
// void func(int(*p)[3], int len)
void func(int p[][3], int len)
{
for (int ii = 0; ii < len; ii++)
{
for (int jj = 0; jj < 3; jj++)
cout << "p[" << ii << "][" << jj << "]=" << p[ii][jj] << " ";
cout << endl;
}
}
int main()
{
int bh[2][3] = { {77,66,99},{77,88,66} };
func(bh, 2);
}
示例15输出结果:
p[0][0]=77 p[0][1]=66 p[0][2]=99
p[1][0]=77 p[1][1]=88 p[1][2]=66
在函数中普通数组在栈上分配内存,栈很小;如果需要存放更多的元素,必须在堆上分配内存。
动态创建一维数组的语法:数据类型 *指针=new 数据类型[数组长度];
释放一维数组的语法:delete [] 指针;
示例16
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main()
{
int *arr=new int[8]; // 创建8个元素的整型数组。
for (int ii = 0; ii < 8; ii++)
{
arr[ii] = 100 + ii; // 数组表示法。
cout << "arr[" << ii << "]=" << *(arr + ii) << endl; // 指针表示法。
}
delete[]arr;
}
示例16输出结果
arr[0]=100
arr[1]=101
arr[2]=102
arr[3]=103
arr[4]=104
arr[5]=105
arr[6]=106
arr[7]=107
–
①动态创建的数组没有数组名,不能用sizeof运算符。
② 可以用数组表示法和指针表示法两种方式使用动态创建的数组。
③必须使用delete[]来释放动态数组的内存(不能只用delete)。
④不要用delete[]来释放不是new[]分配的内存。
⑤ 不要用delete[]释放同一个内存块两次(否则等同于操作野指针)。
⑥ 对空指针用delete[]是安全的(释放内存后,应该把指针置空nullptr)。
⑦ 声明普通数组的时候,数组长度可以用变量,相当于在栈上动态创建数组,并且不需要释放。
⑧ 如果内存不足,调用new会产生异常,导致程序中止;如果在new关键字后面加(std::nothrow)选项,则返回nullptr,不会产生异常。
⑨ 为什么用delete[]释放数组的时候,不需要指定数组的大小?因为系统会自动跟踪已分配数组的内存。
结构体是一种自定义的数据类型,用结构体可以创建结构体变量。在C++中,用不同类型的指针存放不同类型变量的地址,这一规则也适用于结构体。如下:
struct st_muyu muyu; // 声明结构体变量muyu。
struct st_muyu *pst=&muyu; // 声明结构体指针,指向结构体变量muyu。
通过结构体指针访问结构体成员,有两种方法:
(*指针名).成员变量名 // (*pst).name和(*pst).age
指针名->成员变量名 // pst->name和*pst->age
在第一种方法中,圆点.的优先级高于*,(*指针名)两边的括号不能少。如果去掉括号写成(指针名).成员变量名,那么相当于(指针名.成员变量名),意义就完全不一样了。在第二种方法中,->是一个新的运算符。上面的两种方法是等效的,程序员通常采用第二种方法,更直观。与数组不一样的是,结构体变量名没有被解释为地址。
如果要把结构体传递给函数,实参取结构体变量的地址,函数的形参用结构体指针。如果不希望在函数中修改结构体变量的值,可以对形参加const约束。
引用变量是C++新增的复合类型,引用是已定义的变量的别名。引用的主要用途是用作函数的形参和返回值。
引用的本质是指针常量的伪装,编译器会把引用解释为指针,引用和指针从本质上来说没有区别
声明/创建引用的语法:数据类型 &引用名=原变量名;
① 引用的数据类型要与原变量名的数据类型相同。
② 引用名和原变量名可以互换,它们值和内存单元是相同的。
③ 必须在声明引用的时候初始化,初始化后不可改变。
④ C和C++用&符号来指示/取变量的地址,C++给&符号赋予了另一种含义。
示例17
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int main()
{
// 声明 / 创建引用的语法:数据类型 & 引用名 = 原变量名;
int a = 3; // 声明普通的整型变量。
int& ra = a; // 创建引用ra,ra是a的别名。
cout << " a的地址是:" << &a << ", a的值是:" << a << endl;
cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl;
ra = 5;
cout << " a的地址是:" << &a << ", a的值是:" << a << endl;
cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl;
}
示例17输出结果
a的地址是:00000086A498F514, a的值是:3
ra的地址是:00000086A498F514,ra的值是:3
a的地址是:00000086A498F514, a的值是:5
ra的地址是:00000086A498F514,ra的值是:5
–
把函数的形参声明为引用,调用函数的时候,形参将成为实参的别名。这种方法也叫按引用传递或传引用。引用的本质是指针,传递的是变量的地址,在函数中,修改形参会影响实参。下面的例子给出了传值、传地址、传引用的示例:
示例18
#include
using namespace std;
void fout(int num, string str) //传值
{
cout << num << " " << str << endl;
num = 66; str = "MY";
}
void fout2(int* num, string* str) //传地址
{
cout << *num << " " << *str << endl;
*num = 66; *str = "MY";
}
void fout3(int& num, string& str) //传引用
{
cout << num << " " << str << endl;
num = 66; str = "MY";
}
int main()
{
int no; string st;
no = 99; st = "慕羽";
fout(no, st);
cout << no << " " << st << endl;
cout << endl;
no = 99; st = "慕羽";
fout2(&no, &st);
cout << no << " " << st << endl;
cout << endl;
no = 99; st = "慕羽";
fout3(no, st);
cout << no << " " << st << endl;
}
示例18输出结果
99 慕羽
99 慕羽
99 慕羽
66 MY
99 慕羽
66 MY
–
传值、传地址、传引用的差异可概括成下表所示,相比之下,传引用更简洁有效,
《C++ Primer Plus》中给出了传值、传地址和传引用的指导原则
(1)如果不需要在函数中修改实参
如果实参很小,如C++内置的数据类型或小型结构体,则按值传递。
如果实参是数组,则使用const指针,因为这是唯一的选择(没有为数组建立引用的说法)。
如果实参是较大的结构,则使用const指针或const引用。
如果实参是类,则使用const引用,传递类的标准方式是按引用传递(类设计的语义经常要求使用引用)。
(2)如果需要在函数中修改实参
如果实参是内置数据类型,则使用指针。只要看到func(&x)的调用,表示函数将修改x。
如果实参是数组,则只能使用指针。
如果实参是结构体,则使用指针或引用。
如果实参是类,则使用引用。
当然,这只是一些指导原则,很可能有充分的理由做出其他的选择。
此外,传引用不必使用二级指针,如下面的例子所示:
示例19
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void func1(int** p) // 传地址,实参是指针的地址,形参是二级指针。
{
*p = new int(3); // p是二级指针,存放指针的地址。
cout << "func1内存的地址是:" << *p << ",内存中的值是:" << **p << endl;
}
void func2(int*& p) // 传引用,实参是指针,形参是指针的别名。
{
p = new int(3); // p是指针的别名。
cout << "func2内存的地址是:" << p << ",内存中的值是:" << *p << endl;
}
int main()
{
int* p = nullptr; // 存放在子函数中动态分配内存的地址。
//func1(&p); // 传地址,实参填指针p的地址。
func2(p); // 传引用,实参填指针p。
cout << "main 内存的地址是:" << p << ",内存中的值是:" << *p << endl;
delete p;
}
示例19输出结果
func2内存的地址是:000001CB275010F0,内存中的值是:3
main 内存的地址是:000001CB275010F0,内存中的值是:3
如果引用的数据对象类型不匹配,当引用为const时,C++将创建临时变量,让引用指向临时变量。如果函数的实参不是左值或与const引用形参的类型不匹配,那么C++将创建正确类型的匿名变量,将实参的值传递给匿名变量,并让形参来引用该变量。
使用const可以避免无意中修改数据的编程错误。使用const使函数能够处理const和非const实参,否则将只能接受非const实参。使用const,函数能正确生成并使用临时变量。
左值是可以被引用的数据对象,可以通过地址访问它们,例如:变量、数组元素、结构体成员、引用和解引用的指针。非左值包括字面常量(用双引号包含的字符串除外)和包含多项的表达式。
比如,在下面的例子中,由于函数func的形参使用了const修饰,所以调用该函数时,给定的实参可以是常量,而函数func2在调用时,其实参不能是常量,需要先将常量999和“哈哈哈”存放在变量中,再将变量作为实参传给func2,否则会报错。
示例20
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void func(const int& no, const string& str) // 传引用。
{
cout << "华灯初上" <<" " << no << "大道不孤:" << " " << str << endl;
}
void func2(int& no, string& str) // 传引用。
{
cout << "华灯初上" << " " << no << "大道不孤:" << " " << str << endl;
}
int main()
{
func(999,"哈哈哈");
int a = 999;
string st = "哈哈哈";
func2(a, st);
}
示例20输出结果
华灯初上 999大道不孤: 哈哈哈
华灯初上 999大道不孤: 哈哈哈
传统的函数返回机制与值传递类似。函数的返回值被拷贝到一个临时位置(寄存器或栈),然后调用者程序再使用这个值。如double m=sqrt(81); sqrt(81)的返回值9被拷贝到临时的位置,然后赋值给m。如果返回引用则不会拷贝内存。
语法:返回值的数据类型& 函数名(形参列表);
如果返回局部变量的引用,其本质是野指针,后果不可预知。可以返回函数的引用形参、类的成员、全局变量、静态变量。 返回引用的函数是被引用的变量的别名,将const用于引用的返回类型。
示例21
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。
int func( int& num) // 传引用,函数返回值为int型变量。
{
num++;
return num;
}
int &func2(int& num) // 传引用,返回值为引用。
{
num++;
return num;
}
int main()
{
int n = 999;
int n1 = func(n);
int &n2= func2(n);
cout << "n1: " << n1 << " " << "n2: " << n2;
}
示例21输出结果
n1: 1000 n2: 1001
如果类的成员函数中涉及多个对象,在这种情况下需要使用this指针。this指针存放了对象的地址,它被作为隐藏参数传递给了成员函数,指向调用成员函数的对象(调用者对象)。每个成员函数(包括构造函数和析构函数)都有一个this指针,可以用它访问调用者对象的成员。(可以解决成员变量名与函数形参名相同的问题)
*this可以表示对象。
如果在成员函数的括号后面使用const,那么将不能通过this指针修改成员变量。
示例22
User_information.h文件:
#include // 包含头文件。
using namespace std; // 指定缺省的命名空间。ragma once
class User_information
{
private:
//属性
string name_;
int id_;
public:
//构造函数
User_information() { cout<<endl << "这仅仅只是一个构造函数" << endl << endl; }
User_information(string const& name, int const& ID) :name_(name), id_(ID)
{
cout<<endl << "这个构造函数利用初始化列表对属性进行了初始化" << endl << endl;
}
//析构函数
~User_information() { cout << endl << "这仅仅只是一个析构函数" << endl; }
//成员函数
void show_information() const
{
cout << "Name:" << " " << name_ << endl;
cout << "Id:" << " " << id_ << endl;
}
void reset_name(string& re_name) { name_ = re_name; }
void reset_id(int& re_id) { id_ = re_id; }
string get_name() const { return name_; }
int get_id() const { return id_; }
User_information & compare_id(User_information & user)
{
if (user.id_ > id_) return user;
return *this;
}
};
classtest.cpp文件:
#include "User_information.h"
int main()
{
string r_name = "雨夜聆风";
int r_id = 666666;
User_information User; // 创建User_information对象Muyu,不设置任何初始值。
User.reset_name(r_name); // 修改属性值
User.reset_id(r_id);
cout << "Name:" << " " << User.get_name() << endl;
cout << "Id:" << " " << User.get_id() << endl;
User_information Muyu("慕羽",999999); // 创建User_information对象Muyu,并设置初始值。
Muyu.show_information();
User_information& id_max = User.compare_id(Muyu);
cout<<endl << id_max.get_name() << "的id值更大!!!"<<endl;
}
示例22输出结果
这仅仅只是一个构造函数
Name: 雨夜聆风
Id: 666666
这个构造函数利用初始化列表对属性进行了初始化
Name: 慕羽
Id: 999999
慕羽的id值更大!!!
这仅仅只是一个析构函数
这仅仅只是一个析构函数