c/c++指针

1. 传入指针

简单讲就是,在c中,传入函数的所有东西都是一个”副本“,传入的指针实际上是该指针的副本。因此,如果如果你希望一个函数对传入的指针的指向进行修改,那么这个函数是不会作用到这个函数之外的,换而言之,你的修改不能让主函数中的指针指向发生修改。因此,如果你希望通过函数修改指针的指向,那么在c中需要通过指针的指针。
reference_1

2. 传入指针的指针

首先明确两点:

  1. 自增运算符的优先级++高于解引用*
  2. c中所有的传入都是传值(传指针是传入指针值)在函数内对值的修改都是对副本的修改,不会对函数作用外部造成影响''(第一部分)

下面是示例代码:

#include<cstdio>
#include<iostream>
void function(int **s){
    printf("\n");
    // s++;
    // *s++;
    //(*s)++;    
    // **s++;
    // (**s)++;
    std::cout<<s<<std::endl; //指针的指针
    std::cout<<*s<<std::endl; //被指向的指针
    std::cout<<**s<<std::endl; //被指向指针指向的值
}
int main(){
    int num[3] = {1,2,3};
    int *a = num;
    std::cout<<a<<std::endl;
    function(&a);//传入指针的地址 (或者 指向a指针地址的指针)
}

对关键部分的解释:
1. s++
代表指向指针的指针向后移动一位(指针地址加一),这将导致指针丢失,指向未知区域,也有可能报段错误(会直接终止运行)。但这一项操作不会对函数外产生影响。
2. *s++
代表指向指针的指针先向后移动一位(指针地址加一),然后在解引用。因为指向指针的指针在后移之后指向位置区域,因此可能输出未知值,也有可能报段错误(会直接终止运行)。但这一项操作不会对函数外产生影响。
3. (*s)++
代表先对指向指针的指针进行解引用,然后后移一位。在本例中,指针指向的指针指向数组的第一个元素的内存地址,所以结果是指向数组的指针后移,指向数组的第二个元素。这导致指向指针的指针的值发生改变,就是指向数组的指针发生改变。因为是产生副本的指针指向的指针发生了改变,所以会影响到主函数中a指针本身的值。
4. **s++
代表二重指针先后移,然后解引用拿到双重指针指向的指针,然后再次解引用,拿到指针指向的值。因为1的缘故,所以会产生可能的错误或位未知输出。会对住函数外产生影响。
5. (**s)++
对双重指针进行两次解引用,拿到指针指向的值,然后再加一,所以结果是s指向的值加一,也就是数组的第一个值加一。会影响到主函数中的数组。
6. 输出s
输出双重指针的内存地址。
7. 输出*s
输出双重指针解引用后的指针的内存地址,(也就是输出a指针的内存地址)。
8. 输出**s
输出对双重指针进行两次解引用的值,(理解为对双重指针指向的指针进行解引用),结果是a指针解引用后的值(或者a指针指向地址的值)。
于此对应,c++中补充了传引用引用,可以直接通过引用来对传入的参数进行修改(第五部分)。个人理解,这是对指针的指针的一个方便的优化,因为通过传引用引用就可以使函数中的修改直接作用于外部的变量。

3. 野指针和空指针(NULL)

3.1 野指针

指向垃圾内存的指针,往往由于一些不严谨的地方产生。

产生原因:
1.指针变量没有被初始化。这里需要注意,指针初始化后为赋值并不会指向NULL。
2.指针被free或者delete之后,没有设置为NULL,让人误以为这是一个合法指针。因此强调new/delete或者malloc/freeze一定要成对出现。
3.指针操作超越了变量的作用范围。

野指针在调用的时候可能会让程序误认为是一个正常指针,因此操作的时候可能会产生意想不到的问题,因此需要避免这种问题。

3.2 空指针

  1. 空指针不指向任何实际的对象或者函数。反过来说,任何对象或者函数的地址都不可能是空指针。
  2. 空指针是一个特殊的指针,因为这个指针不指向任何地方。这意味任何一个有效的指针如果和空指针进行相等的比较运算时,结果都是false。
  3. 空指针只是一种编程概念,就如一个容器可能有空和非空两种基本状态。
  4. NULL是一个标准规定的宏定义,用来表示空指针常量。在C++里面被直接定义成了整数立即数的0,而在没有__cplusplus定义的前提下,就被定义成一个值是0的 void* 类型的指针常量。
    reference_2

注:指针声明一定要明确指向哪里,否则会产生“野指针”

4. 结构体指针

4.1 声明结构体和声明结构体指针的区别

typedef struct pp{
    int *test;
    int *another;
}pp;
int main(){
    pp P;
    pp *P1;
    printf("%d\n",sizeof(P));
    printf("%d",sizeof(P1));
}
//输出
16
8

从上面给的实例中可以很清楚的看到声明结构体和声明结构体指针的区别:声明结构体就是在栈区分配了一个与结构体大小相同的内存,但是指针只是申请了一个指针类型的内存。如果只是声明了一个结构体指针而没有初始化(new/malloc/指定变量地址),那么该指针就是“野指针”,在调用的时候会出现错误。

4.2 结构体指针和结构体内部的指针都必须初始化

    [reference](https://blog.csdn.net/qq_40285768/article/details/104607834)
    原因于此类似,但是需要注意定义在结构体内部的指针如果需要调用也是需要初始化。

5. c++的传引用引用(&

个人理解:传引用引用是对双指针的优化,他让函数可以直接对指针本身进行修改和操作

5.1

如果在函数声明的时候使用了Element* &S,那么表明是一个传引用引用的指针,是可以直接对指针本身的值进行操作,也会影响到函数外部。因此如果只是想要只读的调用(例如遍历链表),那么可以使用const关键字或者创建一个新的指针指向传入的指针,然后对新的指针进行操作。

补充:声明指针一定要确保指针指向一个地址或者指针申请一块空间,否则会在传入函数指针时出现错误,或者调用的时候导致错误。

以下是对应的示例代码:

#include<cstdio>
#include<iostream>
typedef struct pointer{
   int data;
   struct pointer *next;
}pointer;
void function(pointer* &s){
   pointer *p = new pointer;
   p -> data = 5;
   p -> next = NULL;
   s -> next = p;
   std::cout<<s -> next -> data;
}
int main(){
   pointer *P;
   P = new pointer; //重点!!!!
   function(P);
   std::cout<<P -> next -> data;
   delete P;
}

如果缺少P = new pointer; //重点!!!!这一句为声明后的指针指定的区域,那么在function的调用中会因为s指向的区域不存在而导致segment error

5.2

但如果不是为了使用调用的指针,而是给声明的指针赋值,那么传引用引用是可以,并且能够达到预期的效果。这种常见于数据结构中初始化一个结构的函数的操作中。
例如:

typedef struct pointer{
   int data;
   struct pointer *next;
}pointer;
void function(pointer* &s){
   // pointer *p = new pointer;
   // p -> data = 5;
   // p -> next = NULL;
   // s = p; //与下面的效果一样

   s = new pointer;
   s -> data = 5;
   s -> next = NULL;
   std::cout<<s -> data;
}
int main(){
   pointer *P;
   function(P);
   std::cout<<P -> data;
   delete P;
}
//输出结果
55

自我理解: c++为了能简化在函数中能对指针地址的操作,因此可以使用pointer* &来表示传引用引用一个指针,在函数内对指针的地址进行操作是可以影响到主函数中。在c中传入指针只是传入了指针副本,不能对传入指针本身进行有效操作(这时候就需要双重指针)。

5.3 c++不允许传引用引用一个数组元素

void function(int &edges[6][2]) //这样是不允许的 编译会报错
void function(int &test[6][) //这样是不允许的 编译会报错

具体在第七部分(向函数传入数组指针)

6. 多维数组与指针

首先请记住理解:

多维数组实际上是数组的数组/数组元素为数组的数组

6.1 总结

对于二维数组可以分为以下三种赋值方式:

int a[3][5]; 
...//进行赋值操作

int (*d)[3][5] = &a;
int *e = &a[0][0];
int (*f)[5] = a;

区别在于:
第一种赋值得到的指针的操作和原来的多维数组操作没有区别;
第二种指定的是a数组第一个数组元素的第一个元素,遍历的时候是线性的;
第三种相当于规定了指向a数组元素类型的一个指针,因为a数组的元素类型是一个长度为五的整型数组,因此规定了一个这样类型的指针指向a。

6.2 详细探究过程如下(很长,基于个人理解):

    #include<iostream>
	int main(){
	    using namespace std;
	    int a[3][5]; //a是长度为3的数组 每个元素又都是一个长度为5的数组
	    int p = 0;
	    for(int i = 0; i < 3 ; i++){
	        for(int j = 0; j < 5 ; j++){
	            a[i][j] = p;
	            cout<<p<<'\t';
	            p++;
	        }
	        cout<<endl;
	    }
    //需要谨记 自增运算符/自减的运算优先级都要比*(取值运算符)优先级高  又高于加号/减号运算符==============================
	    int (*d)[3][5] = &a;  //对整个二维数组的第一个元素取地址
	    cout<<(*d)<<"\t"; //输出的是a数组第一个元素的地址
	    cout<<(*d)[0]<<"\t"; //输出的是a数组第一个元素的地址 与上方的输出相同
	    cout<<*d[1]<<"\t";  // 输出的是一个指针 
	    cout<<*(*d)[1]<<"\t";  // 上面的指针指向a的第二元素数组的第一个元素 即a[1][0]
	    cout<<(*d)[2][2]<<'\t'; //输出的是a[2][2]
	    cout<<(*d+1)[2][2]<<'\t'; //未知输出  
	//=============================================
	    cout<<(*d+1)<<'\t'; // 输出一个指针 
	    cout<<*(*d+1)<<'\t'; // 还是一个指针
	    cout<<*(*(*d+1))<<'\t'; // 是a[1][0] => 可以得出 对于赋值给二维数组第一个元素的地址的指针 对其进行运算 就是将a数组的指针向后移动一位
	    cout<<*(*d+1)<<"   "<<&a[1][0]; //不放心 检查一下地址是否一样
	    // => 由上面四个输出推测 对二维数组的第一个元素指针进行运算 那么就是其实就是将指向a数组的指针向后运算 不涉及a数组内的(数组)元素
	    //因此 如果想要遍历a数组的第一个数组元素 那么应该对第一个元素的指针进行运算然后解析
	    cout<<*((*d)[1]+1)<<"\t"; //输出6
	    //检查是6还是5+1 使用地址
	    cout<<(*d)[1]+1<<"\t"<<&a[1][1]; // 地址一样 所以就是
	
	    //其他的尝试输出
	    cout<<*(*(d+1))<<'\t'; // 是一个指针
	    cout<<*(*(*(d+1)))<<'\t'; // 未知输出
	
	    int *e = &a[0][0]; //这样会导致对上面操作的数组发生错误 因此两个赋值并不一致
	    // => 可以理解为 上方是声明了一个[3][5]类型的指针,然后指向a
	    // 下方就是简单的指向a数组第一个元素数组的第一个元素的指针 为了对比 来输出地址进行对比
	    cout<<endl<<e<<"  "<<d<<endl; //地址一致 因此猜测正确 指向的地址一样 但是指针类型不一样
	
	    //接下来对e进行运算
	    cout<<*(e+1)<<"\t"; //输出1,即a[0][1]
	    cout<<*(e+5)<<"\t"<<endl; //输出5 即a[1][0] 发现直接输出a数组第二个数组元素的第一个元素 
	    //  其实也可以解释为 实际上再内存中还是呈线性的 逻辑上还是线性储存的
	
	    // => 猜想d指针是否也能够进行类似操作 而不是解引用之后再对指针进行操作再进行解引用 -> 显而易见不可以
	    //对d指针的更深一步理解 他就是将a数组整体进行了copy 因此他的操作不想e指针那么丝滑(指可以直接线性运算迭代)
	    // 他的操作也和a数组原本的操作更加接近 只不过都换成了指针
	
	//===============================================================
	    cout<<endl;
	    //再来看c++ primer中的二维数组指针
	    int (*f)[5] = a;
	    //通过上面的操作 因此这一下理解更加清晰
	    // => 自我理解:这是一个类型为[指向五个元素数组]的指针 因此对他进行指针运算相当于对a数组进行运算
	    cout<<*f<<"\t"<<**f<<"\t"; //说明这样赋值的指针也是指向a数组第一个数组元素的第一个元素
	    cout<<*((*f)+1)<<endl; // 因此这样就是对第一个数组元素的指针后移后再解引用 所以就是1
	    // 所以现在我希望通过进行指针运算访问a数组内第二个数组的元素
	    cout<<*(*(f+1)+1)<<"\t"; // 猜测正确 f指针就是一个指向a数组的指针 直接对f指针运算+1 得到a数组第二个元素的指针 然后再次+1 代表a的第二个数组的指针向后移 然后解引用 得到6 也就是a[1][1]
	    // 但是为什么二维数组的赋值要这样写? 
	    // 首先需要明确二维数组实际上是数组元素为数组类型的数组
	    // 因此a数组内的元素类型就是一个数组 因此指向a数组的指针就是数组指针的指针 所以才需要这样规定
	    
	    //=> 与此同时 还需要注意区别数组指针和指针的数组
	    //ex. int (*pointer)[10];
	    //ex. int *pointer[10];
	    // 前者是多维数组指针 后者是指针的数组(即pointer这个数组内的元素都是int*(int型指针))
	
	    cout<<endl<<"运行结束"<<endl;
	}

7. 向函数传入数组指针

reference

reference_2
reference_3
reference_4