首页
社区
课程
招聘
《C与指针》指针部分个人学习笔记
2021-4-17 16:05 6900

《C与指针》指针部分个人学习笔记

2021-4-17 16:05
6900

入门逆向,学习了一下《C和指针》,将其中有关指针的两章学习了一下感觉受益匪浅,写出来与大家共同进步,不足之处望各位老师傅指正

《C与指针》第六章-指针

6.1内存与地址

1.在现代的机器上,每个字节包含8位,可以存储无符号数0-255或者有符号数-128-127。为了存储更大的值,我们把两个或更多字节合并在一起作为一个更大的内存单元(1字WORD=2字节BYTE,1双字DOUBLEWORD=2字WORD=4字节BYTE),例如1个中文就是使用1个字表示。

 

2.尽管1个字包含了2个字节,但是其仍然只要一个地址,地址是由最左边的字节表示还是最右边的字节表示取决于机器。注意:在要求边界对齐(字节对齐)的机器上,整型值的存储起始位置一般只能是某些特定的字节,比如2或4的倍数。

 

3.内存中的每个位置都由一个独一无二的地址表示,内存中的每个位置都包含一个值。
4.高级语言提供了通过名字访问内存位置而不是地址,但是名字与内存位置的关联并不由硬件提供,而是编译器帮助我们实现,硬件仍然通过地址访问内存位置。

6.2值和类型

1.内存中存储的是0和1,它到底是什么数据取决于它们被使用的方式,如果使用整型算数指令它们就被解释为整数,如果使用浮点型指令,那他们就被解释为浮点数。故不能通过简单的检查一个值的位来判断它的类型

6.3指针变量的内容

1.指针的初始化是由&操作符完成的,它用于产生操作数的内存地址。例如:p=&a

 

2.一个变量的值就是分配给该变量的内存位置所存储的数值,即时指针变量也不例外。即定义的指针的值是其指向的变量的地址,而非变量地址对应的值(变量本身)。

6.4间接访问操作符

1.用于执行间接访问的是单目操作符*

 

2.设d的值为100,100地址处存放着数据112,那么*d的右值为112--对应位置100的内容,左值为100本身

 

3.指针变量本身就是一个数字,不存在内建的间接访问属性,除非表达式中存在间接访问操作符,否则无法访问指向的地址中存放的数据。

6.5未初始化和非法的指针

1.int*a;*a=12;这是错误的,因为我们未对其进行初始化,所以不知道12会存放在哪里。这样操作运气好会导致报错,运气不好会指向一个合法的地址,导致那个位置的值被修改。

 

2.在对指针进行间接访问之前,必须确保它们已被初始化。

6.6NULL指针

1.NULL指针是一个特殊的指针变量,表示不指向任何东西。返回NULL不好,因为这样会导致用单一的变量表示两种不同的意思(有没有找到元素和找到了的话它是那个元素),应采用返回状态值(提示查找是否成功)加指针(查找成功时它指向的就是找到的元素)的方式。

 

2.对一个NULL指针进行间接访问是违法的,结果因编译器而异,有些机器上会访问内存零,编译器确保内存零不存储任何变量,这样会导致可能不报错。

 

3.如果你已经知道指针将被初始化成什么地址,就把它初始化为该地址,否则就把它初始化为NULL。

6.7指针,间接访问和左值

1.左值:一般为一个对象,可用于赋值运算符左边;右值:一般为一个值,可用于赋值运算符右边。

 

2.指针变量可以为左值不是因为他们是指针,而是因为他们是变量。

 

3.把整型值转换为指针或者把指针转换为整型值是罕见的,一般都为无意识的错误。

6.8指针,间接访问和变量

1.*&a=25等同于a=25;因为&得到了a的地址,*又读取地址对应的空间。

6.9指针常量

1.*(int *)100=25。这种情况是基本不会使用的,因为无法你预测编译器会把某个特定的变量放在内存的什么位置,&只有在执行时才会得到地址值,但是这时候你已来不及去修改源代码中的地址值。

6.10指针的指针

1.指针变量跟其他变量一样,占据内存中的某个特定位置,用&符号同样可以取得他的地址。
2.定义时要注意,若要取int*p的地址,则要定义为int**q。

6.11指针表达式

1
2
3
例如:
char ch='a';
char *cp=&ch;

1.ch作为右值为'a',左值为ch这个内存的地址,即给ch赋值。

 

2.&ch作为右值为ch的地址,左值不合法,原因是 &ch这个表达式未标识任何机器内存的特定位置,所以他不是一个合法的左值(好好理解,跟*cp能够做左值有本质区别)

 

3.cp作为右值表示cp的值(ch的地址),作为左值表示cp所处的内存位置,即给cp赋值。

 

4.&cp作为右值表示指针变量cp的地址,作为左值非法,原因与&ch一样。

 

5.*cp+1作为右值表示先取ch的值,然后加1,最终结果为'b'。作为左值非法,因为有一个加1导致存储位置并未清晰定义。

 

6.*(cp+1)作为右值表示ch后面一个内存空间的值,作为左值表示ch后面一个内存空间的地址。注意:间接访问操作符是少数几个其结果为左值的操作符。

 

7.++cp作为右值代表ch后面一个内存空间的地址,因为前缀++表示先增加他的操作数再返回结果,作为左值非法,因为你相当于是ch后内存空间的地址,只是一个值,所以是存储位置未清晰定义。

 

8.cp++作为右值代表ch的地址,因为后缀++表示先返回结果,再增加cp的值,作为左值非法,原因同上。

 

9.*++cp,间接访问操作符用于增值后的cp,即作为右值表示ch后面一个内存空间的值,作为左值表示ch后面一个内存空间的地址。

 

10.*cp++,间接访问操作符用于未增值的cp,即作为右值表示ch的值,作为左值表示ch的地址。这个原理分为三步:第一是++操作符产生了cp的一份拷贝,第二是++操作符增加了cp的值,第三是在cp的拷贝上进行了间接访问。

 

11.++*cp这个表达式中,作为右值时两个操作符的结合性都是由右向左,所以首先执行间接访问操作,然后cp所指向的地址的值加1,作为左值非法,原因是未清晰定义存储位置。

 

12.(*cp)++这个表达式中,作为右值时首先执行间接访问操作,然后加1,但是注意我们前文提到的后缀++的运算规则,所以实际上结果为ch增值前的值,作为左值非法,原因是未清晰定义存储位置。

 

13.++*++cp这个表达式中,作为右值时首先执行++操作,使cp指向了ch后面一个内存空间,接着执行间接访问操作,得到了ch后面一个内存空间的值,再对其进行加1即为最终值。作为左值非法,原因是未清晰定义存储位置。

 

14.++*cp++这个表达式中,作为右值时,先执行了++操作,但是这个是后缀++,即间接访问操作最终还是得到的是ch的值,然后对ch加1即为最终值。作为左值非法,原因是未清晰定义存储位置。

6.13指针运算

1.当一个指针和一个整数量执行算数运算时,整数在执行加法运算前始终会根据合适的大小进行调整。这个合适的大小即为指针所指类型的大小,调整意为整数值乘以类型的大小。例如:int *p,p指向的int类型大小为4字节;int * *q,q指向的int *类型大小为4字节(指针保存的内存地址大小为32位)

 

2.指针加减整数,这种形式只能用于指向数组中某个元素的指针,数组也可以用这种方式访问,加多少就是数组向后多少位。

 

3.指针减指针,这种用于两个指针都指向同一个数组中的元素时,结果为两个指针在内存中的距离(以数组元素的长度为单位,而非字节为单位)

 

4.关系运算符(>,<,>=,<=),用于比较的两者都指向同一个数组中的元素时,可得出哪个指针指向数组更前或更后的元素。标准未定义如果两个任意的指针进行比较会有什么后果,但是标准允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但是不允许与指向数组第一个元素之前的那个内存位置的指针进行比较

《C与指针》第十三章-高级指针话题

13.1进一步探讨指向指针的指针

1
2
3
4
5
6
7
8
9
10
11
12
int i;
int *pi;
int **ppi;
 
printf("%d\n",ppi);
printf("%d\n",&ppi);
*ppi=5;
ppi=&pi;
*ppi=&i;
i='a';
*pi='a';
**ppi='a';

1.第一个输出语句,如果ppi是个自动变量,那么他就未被初始化,这条语句将打印出来一个随机值。如果他是一个静态变量,则打印0。

 

2.第二个输出语句是把存储ppi的地址打印出来。

 

3.第三个赋值语句,结果是不可预测的,对ppi不应执行间接访问操作,因为其未被初始化。

 

4.第四个赋值语句,将ppi初始化为指向变量pi,这样就可以安全的对ppi执行间接访问操作了。

 

5.第五个赋值语句相当于把pi(通过ppi间接访问)初始化为指向变量i。

 

6.最后三条语句具有相同的结果,既然通过简单的赋值就可以完成的事情,为什么还要使用指针呢?原因是简单赋值并不是总可行的,例如在链表的插入中,变量名在函数的作用域内部是未知的,函数拥有的只是一个指向需要修改的内存位置的指针,所以要对指针进行间接访问操作来访问需要修改的变量。

 

7.只有当确实需要的时候才使用多层间接访问,否则程序将会变得更缓慢,更庞大且更难理解。

13.2高级声明

1.int *f;是如何工作的?实际上是把表达式*f作为一个整体,因此f是指向整型的指针。 这个结论的又一推论为:int *f,g; 这个语句并未声明2个指针变量,g仅仅是一个普通的整型变量。

 

2.int *f( ); 要理解这个表达式的含义,首先必须确定表达式*f()是如何求值的。首先执行的是函数调用操作符(),因为他的优先级高于间接访问操作符。因此,f是一个函数,它的返回值类型是一个指向整型的指针。此时有点像(int *) f( ),即把int*看作一种类型(实际上int*确实是一种类型。。。)

 

3.用于声明变量的表达式和普通的表达式在求值时所使用的规则相同。你不需要为了这类声明学习一套单独的语法。如果你能够对一个复杂的表达式求值,那么你同样可以推断出一个复杂声明的含义,因为其原理相通。

 

4.int (*f)( );这个声明有两对括号,每对含义不同,第二个是函数调用操作符,第一个起到聚组的作用。它使得间接访问在函数调用之前进行,使得f成为一个函数指针,它所指向的函数都返回一个整数值。

 

5.因为每个函数都位于内存中的某个位置,所以存在指向那个位置的指针。 这就是函数指针的由来,函数指针将会在后面详细解释。

 

6.int *(*f)( );它与前一个声明基本相同,f也是一个函数指针,只是所指向的函数返回值是一个整形指针,必须对其进行间接访问操作才能得到整数值。

 

7.int *f[ ];下标的优先级更高,所以f是一个数组,它的元素类型是指向整型的指针。即f是一个指向整型的指针的数组。即可以理解为(int *)f[]。

 

8.int f( ) [];首先这个表达式的含义是,f是一个函数,他的返回值是一个整型数组。这里的问题在于,函数只能返回标量值,不能返回数组,所以它是非法的。

 

9.int f ;这个表达式看起来的意思是:f是一个数组,它的元素类型是返回值为整型的函数。但是这个声明也是非法的,因为数组元素必须具有相同的长度,但是不同的函数显然可能具有不同的长度。

 

10.int (*f[ ])( );这个语句中同样具有两对括号,它们分别具有不同的含义。括号内的*f[]首先进行求值,所以f是一个元素为某种类型的指针的数组。表达式末尾的()是函数调用操作符,所以f肯定是一个数组,数组元素的类型是函数指针,它所指向的函数的返回值是一个整型量。

 

11.int *(*f[ ])( );这个语句与上面的比较相似,主要的区别在于多了一个间接访问操作符,所以这个声明创建了一个指针数组,指针指向的类型是返回值为整型指针的函数。

 

12.注意,以上都是旧式函数声明,括号内未标明形参的类型。

13.3函数指针

1.函数指针最常见的用途是转换表和作为参数传递给另一个函数。

 

2.注意:声明完一个函数变量之后不代表他直接就可以使用,与指针一样,对函数指针执行间接访问之前必须将其初始化为指向某个函数。例如:

1
2
int f(int); //声明函数
int (*pf)(int)=&f;

其中第二个声明创建了函数指针pf,并将其初始化为指向函数f。在函数指针初始化之前具有f的原型(函数原型=函数声明)是很重要的, 否则编译器无法检查f的类型是否与pf所指向的类型一致。注意:声明中第一个int代表pf指向的是一个返回值为int的函数,第二个int代表pf指向的是一个有1个int参数的函数。且声明中第一个括号不可省略,因为函数调用操作符的优先级高于间接访问操作符。

 

3.初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器将其转换为函数指针。&操作符只是显式的说明了编译器隐式执行的任务。

1
2
3
4
int ans;
ans=f(25);
ans=(*pf)(25);
ans=pf(25);

4.函数指针在定义和初始化后,我们有3种方式调用函数。其中第一条语句就是很普通的,但是底层中,函数名f先被转换成一个函数指针,该指针指定函数在内存中的位置。然后,函数调用操作符调用该函数,执行开始于这个地址的代码。 第二条语句对pf执行间接访问操作,它会把函数指针转换为一个函数名,这个转换并非真正需要的,因为编译器在执行函数调用操作符之前,依然会将其转换为函数指针。不过这条语句的效果与第一条相同。第三条语句中,就说明了间接访问操作符的非必要性,因为编译器本身就需要一个函数指针。

13.3.1回调函数

1.一个用户将一个指针函数作为参数传递给其他函数,后者将回调用户的函数,这就是回调函数。有一说一这定义看不太懂,那么用例子来说明,首先至少要有三种类型的函数:主函数,回调函数(独立的功能函数),中间函数(介于主函数与回调函数之间的函数,用于登记回调函数,通知主函数,起桥梁作用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
来看一段Python代码
# 回调函数1
def callback1(x):
    return x * 2
 
# 回调函数2
def callback2(x):
    return x ** 2
 
# 中间函数
def middle(x, func):
    return 100 + func(x)
 
# 主函数
def main():
 
    x = 1
 
    a = middle(x, callback1)
    print(a)
 
    b = middle(x, callback2)
    print(b)
 
    c = middle(x, lambda x: x + 2)
    print(c)
 
main()
 
result:
102
101
103

2.回调函数的执行流程为:主函数需要调用回调函数,中间函数登记回调函数,触发回调函数事件,调用回调函数,响应回调函数。

 

3.回调函数分为阻塞式回调和延迟式回调,也可以称作同步回调和异步回调。区别在于:在阻塞式回调里,回调函数的调用一定发生在主函数返回之前,在延迟式回调里,回调函数的调用有可能是在起始函数返回之后

 

4.用一个精辟的例子来说明回调函数:你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做 触发回调事件,店员给你打电话叫做 调用回调函数,你到店里去取货叫做 响应回调事件。

 

5.搞清楚回调函数之后,我们来讨论C语言中使用它的场景 (C语言中只能使用函数指针实现回调函数,在C++,Python等语言中则可以使用仿函数和匿名函数):假设有一个查找值的函数,但是这个函数用普通方法实现时,存在一个问题,即如果我要查找值的类型不同,就需要重写一个函数实现,这是很麻烦的,所以我们使用函数指针。第一:调用者编写一个函数,用于比较两个值,然后把一个指向这个函数的指针作为参数传递给查找函数,然后查找函数调用这个函数来执行值的比较。第二:我们需要向比较函数传递一个指向值的指针而非值本身(因为我们后面要进行强制类型转换,将其内存中的数据读出,话又说回来了,如果传递的是值本身,那么比较函数对不同的数据类型的值又不能通用了)。

 

6.在使用比较函数中的指针之前,它们必须格外小心的被转换成正确的类型,因为强制转换类型能够躲过一般的类型检查,所以你使用时必须格外小心,确保函数的参数类型是正确的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//范例
#include<stdio.h>
#include"node.h"
 
Node * search(Node *node,void const *value,int (*compare)(void const *,void const *)) //注意最后的形参声明了一个函数指针,它指向的函数返回值为整型,形参为两个void *类型的常量
{
    while(node!=NULL)
    {
        if(compare(&node->value,value)==0)
            break;
        node=node->link;
    }
    return node;
}
 
int compare(void const *a,void const *b)
{
    if( *(int *)a == *(int *)b ) //此处是将a和b先强制转换为指向int的指针,再将其中的数据作为整型读出进行比较
        return 0;
    else
        return 1;
}

13.3.2转换表

1.使用场景,比如有一个很巨大的switch语句,每个分支调用一个子函数,这样会使代码十分复杂,此时我们就可以使用转换表。转换表的本质就是一个函数指针数组,通过数组下标实现不同的函数调用。

1
2
3
4
5
6
7
8
9
10
11
//函数声明
double add(double,double);
double sub(double,double);
double mul(double,double);
double div(double,double);
 
//函数指针数组定义,此处如果去掉中括号,等号前面的部分就相当于声明了一个函数指针,加上了这个中括号,就变成了函数指针数组
double (*open_func[])(double,double)={add,sub,mul,div};
 
//调用,替代了原来switch语句的用处,此处oper为0调用add,为1调用sub以此类推
result=open_func[oper](op1,op2);

2.注意:声明函数指针数组之前一定要确保函数已经声明。转换表与普通数组一样,不可下标越界,千万注意。

13.4命令行参数

1.处理命令行参数是指向指针的指针的另一用武之地,有些操作系统,包括UNIX和MS-DOS,让用户在命令行中编写参数来启动一个程序的执行。这些参数被传递给程序,程序按照它认为合适的任何方式对他们进行处理。

 

2.C程序的main函数具有2个形参,第一个通常为argc,它表示命令行参数的数目。第二个通常为argv,它指向一组参数值。由于参数的数目并没有任何内在的限制,所以argv指向的这组参数值(本质上来说是一个数组)的第一个元素。这些元素的每个都是指向一个参数文本的指针。如果程序需要访问命令行参数,main函数在声明时需要加上这些参数:int main(int argc,char **argv);

 

3.注意:这个数组中每个元素都是一个字符指针,数组的末尾是一个NULL指针。argc的值和这个NULL的值都是用于确定实际传递了多少个参数。argv指向数组的第一个元素,这就是他为什么被声明成一个指向字符的指针的指针的原因。

 

4.还有一点需要注意的是,第一个参数是程序的名称,因为程序显然知道自己的名称,所以通常这个参数是忽略的。不过如果程序采用几组不同的选项进行启动,此时这个参数就有用武之地了。例如UNIX中的ls,l,ll命令都是显示一个目录下的文件,但是效果不同,程序对第一个参数进行检查,就可以确定如何启动它。

 

5.要寻找命令行参数,应使用数组中合适的指针,例如:*++argv(这个表示一个字符串数组的地址)

 

6.处理命令行参数,例如想使循环达到并以非横杆开关的参数时结束,即:*++argv != NULL && **argv == '-',其中后面这个表示的是字符串的第一个字符的值。

 

7.注意在测试**argv之前先测试*argv是很有必要的,因为如果*argv为NULL,那么**argv中的第二个间接访问就是非法的。

13.5字符串常量

1.当一个字符串常量出现在表达式中时,它的值是个指针常量。编译器把这些指定字符的一份拷贝存储在内存的某个位置,并存储一个指向第一个字符的指针。可以对其进行下标引用,间接访问,指针运算等操作。

 

2."xyz"+1,别忘了"xyz"是指向第一个字符的指针,所以这个表达式的意思是指针值加1,得到的是字符y。但是我们会发现,输出的时候好像就不是这么回事了,会输出yz,这是什么原因呢。主要是由于输出函数会循环向后输出直到'\0'为止,下列代码就能帮助我们输出当前表达式的值

1
2
3
4
5
6
7
#include<iostream>
using namespace std;
int main()
{
    cout << *("xyz" + 1) << endl;
    return 0;
}

3.*"xyz",对一个指针进行间接访问操作时,结果就是指针指向的内容。所以表达式的意思是字符x

 

4."xyz"[2],这个表达式的意思与字符串数组相同,就是字符z了

 

5.举个栗子:假设要将一个整型数据转换成十六进制,输出他的个位数,就可以这么写:putchar("0123456789ABCDEF"[value%16]);


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-4-24 16:42 被PlumpBoy编辑 ,原因: 改进
收藏
点赞3
打赏
分享
最新回复 (6)
雪    币: 6677
活跃值: (3295)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
htpidk 2021-4-17 16:36
2
0

4.int *(*f)( );这个声明有两对括号,每对含义不同,第二个是函数调用操作符,第一个起到聚组的作用。它使得间接访问在函数调用之前进行,使得f成为一个函数指针,它所指向的函数都返回一个整数值。 


6.int *(*f)( );它与前一个声明基本相同,f也是一个函数指针,只是所指向的函数返回值是一个整形指针,必须对其进行间接访问操作才能得到整数值。


这两个一样的,4应该是int (*f)( )

雪    币: 763
活跃值: (1075)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
PlumpBoy 2021-4-18 08:38
3
0
htpidk 4.int *(*f)( );这个声明有两对括号,每对含义不同,第二个是函数调用操作符,第一个起到聚组的作用。它使得间接访问在函数调用之前进行,使得f成为一个函数指针,它所指向的函数都返回一个整数值。 ...
非常感谢指正,写的时候和检查的时候都没注意到,过于疏忽大意了
雪    币: 3125
活跃值: (578)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小hanger 2021-4-18 10:26
4
0
"xyz"+1,别忘了"xyz"是指向第一个字符的指针,所以这个表达式的意思是指针值加1,得到的是字符y。
我试了下:输出是 yz
雪    币: 763
活跃值: (1075)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
PlumpBoy 2021-4-18 10:41
5
0
小hanger "xyz"+1,别忘了"xyz"是指向第一个字符的指针,所以这个表达式的意思是指针值加1,得到的是字符y。 我试了下:输出是 yz
感谢指正,不过这个输出的确是yz,我个人是这么理解的:就如同直接输出“xyz”一样,虽然“xyz”表达式本身代表的是一个指向字符串数组首字符的指针,但是在输出时,输出函数会依次输出字符串到'\0'为止。所以使用下面的代码就可以很好的输出当前表达式所对应的值:
#include<iostream>
using namespace std;

int main()
{
       cout << *("xyz" +1) << endl;
       return 0;
}
雪    币: 68
活跃值: (782)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
大鱼吃小鱼 2021-4-18 13:39
6
0

未标识任何机器内存的特定位置;这句话怎么理解,怎么看出来它标识了内存的特定位置

最后于 2021-4-18 13:39 被大鱼吃小鱼编辑 ,原因:
雪    币: 763
活跃值: (1075)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
PlumpBoy 2021-4-18 15:12
7
0
我个人的理解是,通俗一点说是&ch表示读取了ch所在的内存地址,而这个地址客观上是ch存放的位置,&只是读取它,而无法修改,所以无法作为左值。*cp可以作为左值的原因是他是间接访问了cp代表的内存地址上的值,这个值是可以被修改的。
游客
登录 | 注册 方可回帖
返回