07.指针
7.指针
指针也允许pass-by-reference,并且可以用来创造和操作动态数据结构,例如列表、队列、栈和树(lists, queues, stacks and trees)。
7.1 指针变量的声明和初始化
指针变量包含内存地址作为其值,指针包含变量的内存地址,而变量的内存地址又包含一个特定的值。在这个意义上,变量名直接引用一个值,指针间接引用一个值。通过指针引用一个值称为间接引用。 例如下图指针变量的间接引用以及count的直接引用。
7.2 声明指针
指针就像其他变量一样,在使用之前必须先声明,例如上图中的指针,可以声明为:
int* countPtr, count;
指针可以被声明指向任何数据类型的对象。
上诉代码,指针的类型为int*,指针变量的名字为countPtr,可以存放一个int类型数据的地址。
void类型的指针只能用来存放地址,不能用此指针访问内存空间。
7.2.1 初始化指针
无论是在声明时还是在赋值时,都需要将指针初始化为nullptr或内存地址。 具有nullptr的指针"point to nothing",被称为空指针(null pointer).初始化所有指针,防止指向未知或未初始化的内存区域。 将指针初始化为NULL或者0是相同的操作,0是唯一的可以直接赋值给指针的整数,而不用事先将整数类型转化为指针类型(一般使用reinterpret_cast)
7.3 指针操作符
7.3.1 取地址操作符(&)
&是一元操作符,用来取得操作数的地址,例如下列代码:
int y{5}; // declare variable y int* yPtr{nullptr}; // declare pointer variable yPtr yPtr = &y; // assign address of y to yPt
上述代码将变量y的内存地址通过&取得,并赋值给yPtr,现在,指针yPtr间接的引用了变量y的值5。地址运算符不能应用于文字,也不能应用于导致临时值(像计算结果一样)的表达式。
7.3.2 解引用操作符(*)
返回一个左值,表示其指针操作数指向的对象。例如对于上述代码,下列的两段代码的结果是相同的:
cout<<*yPtr<<endl;
cout<<y<<endl;
Using * in this manner is called dereferencing a pointer.还可以作为左值被赋值:
*yPtr=9;
还可以进行如下操作。
7.3.3 使用&和*
#include <iostream> using namespace std; int main() { int a{7}; int* aPtr=&a; cout<<"the address of a is:"<<&a<<"\n the value of aPtr is: "<<aPtr; cout<<"\n\nthe value of a is: "<<a<<"\nthe value of *aPtr is:"<<*aPtr<<endl; }
7.3.4 二级指针
指针(指针变量的简称)用于存放普通变量的地址,二级指针用于存放指针变量的地址。二级指针的声明格式如下:
数据类型** 指针名
使用指针的目的:1.传递地址 2.存放动态分配内存的地址
在函数中,如果传递普通变量的地址,形参用指针;传递指针变量的地址,形参用二级指针。
7.3.5 空指针
用0或者NULL都可以表示空指针。声明指针后,在赋值前让指针指向空,表示没有指向任何地址。C++11建议用nullptr表示空指针,也就是(void*)0。
1.如果对空指针解引用,程序会崩溃。
2.在函数中,应该有判断形象是否为空指针的代码,目的是保证程序的健壮性。
出现野指针的情况有三种:
1.指针在定义时,如果没有进行初始化,它的值是不确定的。
2.如果用指针指向了动态分配的内存,内存被释放以后,指针不会置空,但是,指向的地址已经失效。
3.指针指向的变量已经超过变量的作用域(变量的内存空间已经被收回)
避免方法:
- 指针在定义时,如果没有可以指向的地址,就初始化为nullptr。
- 动态分配的内存释放以后,将其置为nullptr。
- 函数不要返回局部变量的地址。
7.3.6 野指针
野指针即指针指向的不是一个有效(合法)的地址。在程序中访问野指针可能会造成程序的崩溃。
7.4 函数的参数传递
在C++中有三种方式将实参传递给函数:
1.pass-by-value.(传值)
2.pass-by-reference with a reference argument.(传引用)
3.pass-by-reference with a pointer argument.(传地址)
指针和引用一样,也可以用来修改调用者中的变量或者通过引用传递大数据对象,避免复制对象的开销。
7.4.1 Pass-By-Reference with a Pointer Actually Passes the Pointer By Value
用指针通过引用传递一个变量实际上没有通过引用传递任何东西(一个指向该变量的指针被值传递并复制到函数对应的指针参数中)。被调用的函数只需解引用指针就可以访问调用者的该变量,从而完成pass-by-reference。
7.4.2 三种传递方式的使用原则
1)不需要在函数中修改实参
1.如果实参很小,如C++内置的数据类型或小型结构体,则按值传递
2.如果实参是数组,则使用const指针,这是唯一的选择(无法为数组建立引用)
3.如果实参是较大的结构体 ,则使用const指针或const引用
4.数据实参是类,则使用const引用,传递类的标准方式是按引用传递
2)需要在函数中修改实参
1.如果实参是内置数据类型,则使用指针
2.如果实参是数组,则只能使用指针
3.如果实参是结构体,则使用指针或引用
4.如果实参是类,使用引用
7.5 built-in arrays(内置数组)
在前面的内容,介绍了array,一种固定大小的元素的列表。现在介绍另一种固定大小的数据结构built-in arrays.
7.5.1声明和访问built-in arrays
声明的格式如上,arraysize必须是大于零的整数常量。 [ ]不会提供边界检查的功能。
7.5.2 初始化bulit-in arrays
如果提供比元素个数更少的初始化值,剩下的元素都是值初始化的-基本数值类型设置为0,bools设置为false,指针设置为nullptr,类对象由它们的默认构造函数初始化。如果提供过多的初始化值,就会出现编译错误。
如果从带有初始化值列表的声明中省略了built-in数组的大小,则编译器将built-in数组大小调整为初始化器列表中的元素个数。
数组在内存中占用的空间是连续的。
7.5.3 将数组作为参数传递给函数
数组的名字就是数组第一个元素的地址。所以arrayName就等同于&arrayName[0].因此就不需要使用&获取数组的地址从而传递给函数,可以直接传递数组的名字。
正如在之前中所看到的那样,接收到调用者中变量的指针的函数可以修改调用者中的变量。对于内置数组,这意味着被调用的函数可以修改调用者内置数组的所有元素- -除非该函数在对应的内置数组参数前加上const表示不应该修改元素。
7.5.4 声明内置数组形参
可以使用上述代码在函数头中声明内置数组。与array对象不一样,内置数组不知道自身的大小,所以函数的形参列表应该同时包括内置数组以及数组的大小。
一维内置数组用于函数的参数时,只能传递数组的地址,并且要把数组的长度也传递进去,除非数组中有最后一个元素的标志。
上述代码还可以写为如下形式:
当声明一个内置数组形参时,为了清晰起见,使用[ ]符号而不是指针符号。
7.5.5 C++11: Standard Library Functions begin and end
sort函数也可以用在内置数组里,例如:
begin和end函数的头文件为<iterator>。都将内置数组作为实参,并返回一个指针,该指针可用于表示C++标准库函数中需要处理的元素范围,如sort函数。
7.5.6 内置数组的限制
内置数组有几个限制:
1.They cannot be compared using the relational and equality operators—you must use a loop to compare two built-in arrays element by element.
2.They cannot be assigned to one another—an array name is effectively a pointer that is const.
3.They don’t know their own size—a function that processes a built-in array typically receives both the built-in array’s name and its size as arguments.
4.They don’t provide automatic bounds checking—you must ensure that array-access expressions use subscripts that are within the built-in array’s bounds.
7.5.7 Built-In Arrays Sometimes Are Required
一些情况下,必须使用内置数组,例如处理程序的command-line arguments。
7.5.6 清空内置数组
用memset()函数可以将内置数组的全部元素清零。(只适用于C++基本数据类型)
C 库函数 void *memset(void *str, int c, size_t n)** 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
此函数的原型:
void *memset(void *str, int c, size_t n) //str -- 指向要填充的内存块。 //c -- 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。 //n -- 要被设置为该值的字符数。
#include <stdio.h> #include <string.h> int main () { char str[50]; strcpy(str,"This is string.h library function"); puts(str); memset(str,'$',7); puts(str); return(0); }
7.5.7 复制内置数组
C 库函数 void *memcpy(void *str1, const void *str2, size_t n)** 从存储区 str2 复制 n 个字节到存储区 str1。
下面是 memcpy() 函数的声明:
void *memcpy(void *str1, const void *str2, size_t n)
- str1 -- 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
- str2 -- 指向要复制的数据源,类型强制转换为 void* 指针。
- n -- 要被复制的字节数。
// 将字符串复制到数组 dest 中 #include <stdio.h> #include <string.h> int main () { const char src[50] = "http://www.runoob.com"; char dest[50]; memcpy(dest, src, strlen(src)+1); printf("dest = %s\n", dest); return(0); }
7.6 指针和const
Many possibilities exist for using (or not using) const with function parameters, so how do you choose the most appropriate? 应该使用最小特权原则,总是赋予一个函数足够的访问其参数中的数据的权限来完成其指定的任务,但not more。
这一节就是讨论如何使用const和指针实现上述的最小特权原则。
有四种方式将指针传递给函数:
1.a nonconstant pointer to nonconstant data,
2.a nonconstant pointer to constant data
3.a constant pointer to nonconstant data
4.a constant pointer to constant data
7.6.1 nonconstant pointer to nonconstant data
最高的访问权限是由非const指针授予非const数据:
1.数据可以通过解引用指针进行修改
2.指针可以修改指向其他数据
7.6.2 nonconstant pointer to constant data(常量指针)
常量指针就是指向常量值的一个指针变量:
1.一个指针可以被修改为可以指向任意合适类型的数据。
2.指针所指向的数据不能通过该指针进行修改(即,不能通过解引用的方法修改)。
这种类型的指针可以声明为:const int * countPtr 即countPtr是一个nonconstant pointer指向constant data
#include <iostream> void f(const int*) int main(){ int y{0}; f(&y); } void f(const int* xPtr){ *xPtr=100; }
const int x=1; const int* p1; p1=&x; *p1=10;//错误! char* s1="hello";//错误! const char* s2="hello";//正确!
上述代码试图通过指针修改const类型的数据,就会出现错误。
一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值。
当一个函数被调用,且用内置数组作为实参,它(内置数组)的内容被有效的pass-by-reference。因为内置数组的名字被隐式的转换为数组第一个元素的地址。但是在默认情况下,array和vector对象是pass-by-value(整个对象的副本被传递)。对对象中的每个数据项做副本并存储在函数调用栈上需要消耗执行时间。当对象的指针被传递,只有对象地址的副本必须传递,而对象本身不需要。
如果数据不需要被被调用函数修改,使用constant data的指针或者引用传递大型的对象,可以减少使用pass-by-value使用副本带来的开销。还可以提供数据数值传递的安全性。
7.6.3 Constant Pointer to Nonconstant Data(指针常量)
A constant pointer to nonconstant data is a pointer that:
1.永远指向相同的内存地址
2.该内存地址的数据可以通过指针修改
指针在声明为const时,必须初始化,但是如果指针是函数的形参,则使用传递给函数的指针对其进行初始化。
int x=1,y=1; int* const p2=&x; p2=10;//correct!x=10 p2=&y;//error!指针p2是constant的
数组名就是数组的首地址的别名。可以说数组名就是一个指针常量。
7.6.4 Constant Pointer to Constant Data
最小访问权限由const指针授予const数据:
1.指针永远指向相同的内存地址
2.该内存地址的数据不能通过指针修改
这就是一个内置数组应该如何传递给一个只从数组中读取的函数,使用数组下标符号,而不对其进行修改。
#include <iostream> using namespace std; int main() { int x{5},y; const int* const ptr{&x}; cout<<*ptr<<endl; *ptr=7; ptr=&y }
上述代码试图修改const指针和const数据,就会出现错误。
7.7 sizeof操作符
当对内置数组使用sizeof操作符以后,sizeof会返回一个size_t类型的内置数组的总字节数。当在函数中使用指针作为形参,接收内置数组作为实参,sizeof操作符返回的是指针的字节数,而不是内置数组的总字节数。
#include <iostream> using namespace std; size_t getSize(double*); int main() { double numbers[20]; cout<<"the number of bytes in the array is "<<sizeof(numbers); cout<<"\nthe number of bytes returned by getSize is "<<getSize(numbers)<<endl; } size_t getSize(double* ptr){ return sizeof(ptr); }
计算内置数组含有多少个元素可以使用如下命令:
sizeof numbers / sizeof(numbers[0])
下列程序使用sizeof操作符判断基本数据类型、内置数组和指针的字节数
当sizeof的操作数是一个表达式时,与sizeof一起使用的括号是不需要的。记住sizeof是一个编译时(compile-time)操作符,所以它的操作数在运行时不被评估。
7.8 指针表达式及指针算数运算
指针的算数运算只适用于指向内置数组的指针。
一个指针可以被递增( + + )或递减( -- ),一个整数可以被加到一个指针( +或+ =)或从一个指针( -或- =)中减去,或者一个指针可以从另一个相同类型的指针中减去- -这种特殊的操作只适用于指向同一个内置数组的两个指针。
7.8.1 对指针进行加减整数操作
对指针加上整数,并不是直接将此整数加到指针的值上,例如,对于上述的指针vPtr,原始值为3000,命令3000+2,vPtr的值并不是3002,加上的整数还要乘以指针指向的对象的字节数,即vPtr=3000+(2*4)=3008(认定指针指向的内存对象的大小为4个字节)。
7.8.2 指针相减
指向同一内置数组的指针变量可以相互减去。
例如,指针变量v1和v2分别包含地址3008和3000,
x=v2-v1;
会将v2和v1之间的元素个数的值赋给变量x,x=2。Pointer arithmetic is meaningful only on a pointer that points to a built-in array. 我们不能假设同一类型的两个变量在内存中连续存储,除非它们是内置数组的相邻元素。
7.8.3 指针赋值
如果两个指针的类型相同,就可以将一个指针赋给另一个指针。否则,就必须使用cast operator(一般是reinterpret_cast),将赋值语句的右边的指针的类型转换为左侧指针的类型。该规则的例外是指向void (即, void )的指针,它是一个通用指针,能够表示任何指针类型。
说可以用任意类型的指针对 void 指针对 void 指针赋值,不需要类型转换。如果要将 void 指针赋给其他类型的指针,则需要强制类型转换。
7.8.4 Cannot Dereference a void*
一个void*的指针不能进行解引用操作。因为void*的指针指向的是未知类型的内存地址。
7.8.5 指针相比较
指针可以使用等式和关系运算符进行比较。使用关系运算符比较指针是无意义的,除非两个指针指向的是同一个内置数组。指针之间的比较,比较的是指针存储的地址。A common use of pointer comparison is determining whether a pointer has the value nullptr, 0 or NULL (i.e., the pointer does not point to anything).
7.9 指针和内置数组的关系
指针可以用来做任何涉及数组下标的操作。
考虑如下的声明:
int b[ 5 ]; // create 5-element int array b; b is a const pointer int* bPtr; // create int pointer bPtr, which isn't a const pointer
可以将数组的第一个元素的地址赋给指针变量
bPtr=b;
上诉语句和下述代码效果相同
bPtr=&b[0];
而
b+=3;
会造成编译错误,因为它试图通过指针算数运算修改内置数组的名字。
7.9.1 Pointer/Offset Notation
还是考虑上述的代码,内置数组的第三个元素,即b[3],可以用指针表示为:
*(bPtr + 3)
3代表指针的偏移量。当指针指向一个内置数组的起始元素时,偏移量表示应该引用哪个内置数组元素,且偏移量的值与下标相同。这种表示法被称为Pointer/Offset Notation。括号是必要的,因为括号的优先级高于+。在没有括号的情况下,前面的表达式会在* bPtr的值(即在b 中加入3 ,假设bPtr指向内置数组的开始)的副本中加上3。
正如内置数组的元素可以用指针表达式引用一样,地址:
&b[3]
可以写为:
bPtr + 3
7.9.2Pointer/Offset Notation with the Built-In Array’s Name as the Pointer
内置数组名可以作为指针处理,用于指针算术运算。例如
*(b + 3)
也表示数组的第三个元素。
一般情况下,所有带下标的内置数组表达式都可以用一个指针和一个偏移量写出。在这种情况下,使用指针/偏移量表示法,内置数组的名称作为指针。前面的表达式不修改内置数组的名称;b仍然指向内置数组的第1个元素。
7.9.3 Pointer/Subscript Notation
指针可以带下标正如内置数组一样。例如,表达式
bPtr[1]
refers to b[1]; this expression uses pointer/subscript notation.
7.9.4 Demonstrating the Relationship Between Pointers and Built-In Arrays
四种表示方法:
1.array subscript notation
2.pointer/offset notation with the built-in array’s name as a pointer
3.pointer subscript notation
4.pointer/offset notation with a pointer
#include <iostream> using namespace std; int main() { int b[]{10,20,30,40}; int* bPtr{b}; // output built-in array b using array subscript notation cout<<"array b displayed with:\n\narray subscript notation\n"; for(size_t i{0};i<4;++i){ cout<<"b["<<i<<"]="<<b[i]<<'\n'; } // output built-in array b using array name and pointer/offset notation cout<<"\npointer/offset notation where"<<"the pointer is the array name\n"; for(size_t offset1{0};offset1<4;++offset1){ cout<<"*(b+"<<offset1<<")="<<*(b+offset1)<<'\n'; } // output built-in array b using bPtr and array subscript notation cout<<"\npointer subscript notation\n"; for(size_t j{0};j<4;++j){ cout<<"bPtr["<<j<<"]="<<bPtr[j]<<'\n'; } // output built-in array b using bPtr and pointer/offset notation cout<<"\npointer/offset notation\n"; for(size_t offset2{0};offset2<4;++offset2){ cout<<"*(bPtr+"<<offset2<<")="<<*(bPtr+offset2)<<'\n'; } }
运行结果为:
7.10 基于指针的字符串(Pointer-Based Strings)
字符和字符常量:
字符是C + +源程序的基本构件。一个程序可能包含字符常量。字符常量是在单引号中表示为字符的整数值。
字符串:
字符串是作为单个单位处理的一系列字符。 一个字符串可能包括字母、数字和各种特殊字符,如+、-、*、/和$。C++中的字符串字面量(String literals),或字符串常量,用双引号写成如下:
基于指针的字符串:
基于指针的字符串是一个存储字符的内置数组,并以空字符(null character('\0'))结尾,其标记了字符串在内存中终止的位置。字符串通过指针访问其第一个字符。字符串文本sizeof的结果是包含终止空字符的长度。
字符串字面量作为初始化值:
当声明内置字符串数组包含字符串时,内置数组必须足够大,以存储字符串及其终止空字符。
没有在一个内置的字符数组中分配足够的空间来存储终止字符串的空字符是一个逻辑错误。
创建或使用不包含终止空字符的C字符串会导致逻辑错误。
在内置字符数组中存储字符串时,确保内置数组足够大,以容纳将要存储的最大字符串。C + +允许任意长度的字符串。如果一个字符串比待存储字符串的内置数组长,那么内置数组末尾以外的字符将跟随内置数组覆盖内存中的数据,从而导致逻辑错误和潜在的安全漏洞。
访问C字符串中的字符
由于C字符串是一个内置的字符数组,我们可以直接用数组下标记法访问字符串中的单个字符。例如,在前面的声明中,color[0]是字符' b ',color[2]是字符' u ',color[4]是空字符。
用cin读取字符串到char内置数组中
一个字符串可以使用cin读入到一个内置的char类型数组中,例如,下面的语句读取一个字符串到内置的20个元素char数组中:
cin>>word;
用户输入的字符串存储在word数组中。上述语句读取字符,直到遇到空白字符或EOF字符才会停止。字符串不应超过19个字符,为终止空字符留有余地。使用setw流操作符可以保证读入word的字符串不超过内置数组的大小。 例如:
cin>>setw(20)>>word;
规定cin最多读取19个字符到word,并保留第20个位置,用于存储字符串的终止空字符。The setw stream manipulator is not a sticky setting—it applies only to the next value being input.如果输入了19个以上的字符,剩下的字符虽然没有保存在word中,但是会在输入流中,可以被下一次的输入操作读取。
EOF(end of file)就是文件的结束,通常来判断文件的操作是否结束的标志。
EOF不是特殊字符,而是定义在头文件<stdio.h>的常量,一般等于-1;
在微软的DOS和Windows中,读取数据时终端不会产生EOF。此时,应用程序知道数据源是一个终端(或者其它“字符设备”),并将一个已知的保留的字符或序列解释为文件结束的指明;最普遍地说,它是ASCII码中的替换字符(Control-Z,代码26)。
在C语言中,或更精确地说成C标准函数库中表示文件结束符(end of file)。在while循环中以EOF作为文件结束标志,这种以EOF作为文件结束标志的文件,必须是文本文件。在文本文件中,数据都是以字符的ASCII代码值的形式存放。我们知道,ASCII代码值的范围是0~127,不可能出现-1,因此可以用EOF作为文件结束标志。
档案存取或其它 I/O 功能可能传回等于象征符号值 (巨集) EOF 指示档案结束的情形发生。实际上 EOF 的值通常为 -1,但它依系统有所不同。巨集 EOF会在编译原始码前展开实际值给预处理器。
C语言中,EOF常被作为文件结束的标志。还有很多文件处理函数处错误后的返回值也是EOF,因此常被用来判断调用一个函数是否成功。
Reading Lines of Text into Built-In Arrays of char with cin.getline
在某些情况下,希望将整行文本输入到一个内置的字符数组中。为此,cin对象提供成员函数getline,它包含三个参数- -一个内置的用于存储文本行的char数组、一个长度和一个分隔符。
A delimiter is one or more characters that separate text strings. Common delimiters are commas (,), semicolon (😉, quotes (", '), braces ({}), pipes (|), or slashes (/ ). When a program stores sequential or tabular data, it delimits each item of data with a predefined character.
函数在遇到分隔符'\n'时停止读取字符,在输入EOF符时或者输入的字符数量比函数的第二个参数少1时停止读取字符。内置数组中的最后一个字符被保留为终止空字符。如果最后一个字符遇到'\n',会读取后将其丢弃。函数的第三个参数是默认的,所以上述代码可以写为:
cin.getline(sentence,80);
使用cout可以将char数组的内容输出:
cout<<sentence;
像cin一样,cout并不关心内置char数组有多大。字符被输出,直到遇到一个终止的空字符;空字符不显示。 [注: cin和cout假设char的内置数组应该被处理成以空字符结尾的字符串。cin和cout没有为其他内置数组类型提供类似的输入输出处理能力]
7.11 Smart Pointers(智能指针)
带指针的动态内存管理,它允许你在执行时根据需要创建和销毁对象。对这一过程的不当管理是导致细微错误的根源。我们将讨论"智能指针",它通过提供内置指针之外的附加功能,帮助您避免动态内存管理错误。
提供了三种智能指针:
1.unique_ptr:不允许多个指针共享资源,可以用标准库中的move函数转移指针
2.shared_ptr:多个指针共享资源
3.weak_ptr:可以复制shared_ptr,但其构造或者释放对资源不产生影响
7.12 左值、纯右值和将亡值
区分左值右值的真正说法是:能否用“取地址&”运算符获得对象的内存地址。
对于临时对象,它可以存储于寄存器中,所以是没办法用“取地址&”运算符;
对于常量,它可能被编码到机器指令的“立即数”中,所以是没办法用“取地址&”运算符;
这也是C/C++标准的规定。
字符串字面值常量是C++标准中明确指明的特例,为常量左值,所以可以取地址&运算,其地址属于进程的只读内存空间。其它的字面值常量都是“纯右值”
左值:例如变量、数组元素、结构体成员、引用和解引用的指针
1.左值
指定了一个函数或者对象,它是一个可以取地址的表达式
int lv1{ 42 }; // Object int main() { int& lv2{ lv1 }; // Lvalue reference to Object int* lv3{ &lv1 }; // Pointer to Object } int& lv4() { return lv1; } // Function returning Lvalue Reference
1.1左值例子
(1) 解引用表达式*p
(2) 字符串字面量"abc"
(3) 前置自增/自减表达式 ++i / --i
(4) 赋值或复合运算符表达式(x=y或m*=n等)
2.C++11: 纯右值
是不和对象相关联的值(字面量)或者其求值结果是字面量或者一个匿名的临时对象
2.1纯右值例子
(1) 除字符串字面量以外的字面量,比如 32, 'a'
(2) 返回非引用类型的函数调用 int f() { return 1;}
(3) 后置自增/自减表达式i++/i--
(4) 算术/逻辑/关系表达式(a+b、a&b、a<<b)(a&&b、a||b、~a)(a==b、a>=b、a<b)
(5) 取地址(&x)
左值可以当成右值使用
3.C++11: 将亡值
xvalue(eXpiring Value,将亡值):将亡值也指定了一个对象,是一个将纯右值转换为右值引用的表达式
int prv(int x) { return 6 * x; } // pure rvalue int main() { const int& lvr5{ 21 }; // 常量左值引用可引用纯右值 int& lvr6{ 22 }; // 错!非常量左值引用不可引用纯右值 int&& rvr1{ 22 }; // 右值引用可以引用纯右值 int& lvr7{ prv(2) }; // 错!非常量左值引用不可引用纯右值 int&& rvr2{ prv(2) }; // 右值引用普通函数返回值 rvr1 = ++rvr2; // 右值引用做左值使用 }
7.12.1 右值引用
&&是右值引用,函数返回的临时变量是右值
7.13 指针类型的函数
即函数的返回类型为指针类型,需要注意的是:不要将非静态局部地址用作函数的返回值,因为非静态的局部变量的地址在函数执行完毕以后,就销毁了。
正确的例子:1.主函数中定义的数组,在子函数中对该数组的元素进行某种操作以后,返回其中一个元素的地址,这就是合法有效的地址。2.在子函数中通过动态内存分配操作取得的内存地址返回给主函数是合法有效的,但是内存分配和释放不在同一级别,要注意不能忘记释放(在主函数中进行释放),避免内存泄漏。
7.14 函数指针
如果把函数的地址作为参数传递给函数,就可以在函数中灵活的调用函数。
使用函数指针的步骤:
1.声明函数指针
2.使函数指针指向函数的地址
3.通过函数指针调用函数
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。声明函数指针时,必须提供函数的类型,函数的类型就是返回值和参数列表。例如:
int func(int a,string b);
上述函数的函数指针可以声明为;
int (*FuncPtr)(int,string)
*FuncPtr两端的括号必不可少。如果不写括号,则FuncPtr是一个返回值是int指针的函数。
当我们把函数名作为一个值使用时,该函数自动地转换为指针。
FuncPtr = length;//FuncPtr指向名为length的指针 FuncPtr = &length;//和上述语句等价。取地址符是可选的
使用时可以直接使用指向函数的指针调用该函数,不需要解引用操作:
int b1=FuncPtr(7,"hello"); int b1=(*FuncPtr)(7,"hello");//和上述语句的调用等价 int b1=length(7,"hello");//直接调用函数
指向不同函数类型的指针之间不存在转换规则。
需要注意的是,指向函数的指针变量没有 ++ 和 -- 运算。
#include <iostream> using namespace std; int Max(int, int); //函数声明 int main() { int(*p)(int, int); //定义一个函数指针 int a, b, c; p = Max; //把函数Max赋给指针变量p, 使p指向Max函数 cout<<"please enter your number: "<<endl; cin>>a>>b; c = p(a, b); //通过函数指针调用Max函数 cout<<a<<b<<c<<endl; } int Max(int x, int y) //定义Max函数 { int z; if (x > y) { z = x; } else { z = y; } return z; }
7.15 指针数组
指针数组即数组的元素是指针类型,例如:
Point *pa[2];//由pa[0]和pa[1]两个指针组成
7.16 内置数组的排序
C++的内置函数qsort()可以用来对内置数组的元素进行排序。
函数的原型为:
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
- base -- 指向要排序的数组的第一个元素的指针。
- nitems -- 由 base 指向的数组中元素的个数。
- size -- 数组中每个元素的大小,以字节为单位。
- compar -- 用来比较两个元素的函数。
函数不返回任何值。
#include <stdio.h> #include <stdlib.h> int values[] = { 88, 56, 100, 2, 25 }; int cmpfunc (const void * a, const void * b) { return ( *(int*)a - *(int*)b ); } int main() { int n; printf("排序之前的列表:\n"); for( n = 0 ; n < 5; n++ ) { printf("%d ", values[n]); } qsort(values, 5, sizeof(int), cmpfunc); printf("\n排序之后的列表:\n"); for( n = 0 ; n < 5; n++ ) { printf("%d ", values[n]); } return(0); }
7.17 对象指针
对象指针的定义形式:
类名 *对象指针名; Point a{5,10}; Point *ptr; ptr=&a;
7.18 简化的C++内存模型
将内存划分为4个部分
1.stack(栈)
编译器自动分配释放,栈向低地址方向生长。
2.heap(堆)
一般由程序员分配释放,若程序员不释放,程序结束可能由操作系统回收。在C++中,即使用new和delete运算符实现。堆向高地址方向生长。
3.global/static(全局区/静态区)
全局变量和静态变量的存储是放在一块的。
可以简单的认为:
程序启动全局/静态变量就在此处
程序结束后释放
4.constant(常量区)
可以简单理解为所有常量都放在一块
该区域内容不可修改
7.19 using,typedef和#define的用法
define和typedef:
#define是一个预处理指示符
1.用来定义宏。编译器不做类型检查
#define TRUE 1 //结尾无分号,宏名为TRUE //会将程序中所有出现TRUE的地方替换为1
typedef创建能在任何位置替代类型名的别名
typedef someType newTypeName 示例: typedef _Bool bool;//将bool作为类型_Bool的别名
用using替代typedef:
语法:using identifier=type-id
//类型别名,等同于typedef unsigned int UINt; using UInt=unsigned int;//名称'UInt'现在指代类型:unsigned int UInt x=42u; //类型别名,等同于typedef void (*FuncType)(int,int); using FuncType=void(*) (int,int); //'FuncType'现在指代指向函数的指针 void example(int,int){} FuncType f=example;
定义模板的别名,只能使用using
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现