真的理解C语言么?

您所在的位置:网站首页 用指针实现一维数组各元素加和并输出 真的理解C语言么?

真的理解C语言么?

2023-04-02 04:22| 来源: 网络整理| 查看: 265

彻底理解这种话说不出口,能越来越接近彻底理解即可《征服C指针》笔记并不能涵盖书中的巧妙,还是希望大家可以自行阅读,把本文当作一个备忘录

blog链接:真的理解C语言么?–《征服C指针》笔记

前言

使用C语言开发了大半年了,对其中一些奇怪的约束和用法表示迷惑和不理解,《征服C指针》这本书是周末偶尔发现的,大致了解了一下,“这就是我要的”脱口而出,能让自己在热爱的计算机领域进步的书籍总是让人兴奋的,更何况是C语言相关的并且通俗易懂。

不将学到的知识记录下来甚是可惜,于是有了这篇文章。

打好基础C语言是什么样的语言

C语言曾经是只能使用标量的语言。

什么是标量呢?:char、int、double、枚举类型等算术类型以及指针。相对的,像数组、结构体和联合体这种由多个标量组合而成的类型,我们称为聚合类型

早期C语言能够一起实现的功能,只有将标量这种小巧的类型从右边放到左边(赋值),或者标量间的运算、标量间的比较等。

所以为什么不支持if (str == "abc")的原因浮出水面,字符串不是标量,他是char类型的数组,在C中不能用==一下子对数组里的所有元素进行比较。

关于指针

关于指针,K&R中有如下说明:

指针是一种保存变量地址的变量。在C语言中,指针的使用非常广泛

这里先介绍一下变量的概念,C程序里使用的变量值是被保存在内存中的。也就是说,各个变量都被分配了某个地址的内存。向变量赋值,就是把值保存在这个地址的内存中。在C语言中,单是保存整数的变量就有诸如char类型、short类型、int类型和long类型等多种类型。用来保存变量的内存上的空间叫做对象,而被保存为对象的数据类型叫做对象类型

此外,C语言标准对于“指针”一词是如下定义的:

指针类型可以由函数类型、对象类型或不完全类型派生,派生指针类型的类型被称为引用类型

所以,可以这么概括:

指针类型是类型指针类型由其他类型派生而来,例如其类型可以为指向int的指针类型指针类型也是类型,也存在指针类型的变量,指针类型的值先有指针类型,因为有了指针类型,所以有了指针类型的变量和指针类型的值指针类型的值,实际上就是内存的地址

借着这个机会,纠正一下读法:

// 指向int的指针 类型 的 变量hoge_p int *hoge_p;

再看一段神奇的代码:

int *p = 3; // 警告 int *p = 0; // 无警告

为什么第二行代码没有警告?在C语言中,在应当被当作指针处理的上下文中,0这个常量会被当作空指针处理。

关于数组下标运算符[]与数组毫无关系!

选自书中原文,一开始就亮明这句话,体现它的重要性。

关键就在于,在表达式中,不论数组名后是否加[],数组都会被解读成指向其初始化元素指针,所以,有一句话是错的。

在C语言中,如果数组名后不加[],而只是写数组名,那么此名称就表示“指向数组初始元素的指针”

可以注意到,是表达式中,因为声明时用的*、&、[]与表达式中的他们是风马牛不相及的。

而且下标运算符也是一种运算符,他需要访问下标和指针,也算是一个二元运算符,既然二元运算符a + b可以改写为b + a,那么同理如下:

#include ​ int main(void) { int array[5]; int *p; ​ for (int i = 0; i

整体还是能猜出来的:

调用方将实参的值从后往前压入栈中函数参数优先传递给了寄存器edi,esi,add_func中,将两个寄存器的值赋值给局部变量a,ba,b求和,最终放入eax,调用约定规定函数的返回值要保存在寄存器eax中可变长参数typedef char* va_list; void va_start ( va_list ap, prev_param ); /* ANSI version */ type va_arg ( va_list ap, type ); void va_end ( va_list ap );

说明:

1)va_list:一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。

2)va_start:对ap进行初始化,让ap指向可变参数表里面的第一个参数。第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;

3)va_arg: 获取参数。它的第一个参数是ap,第二个参数是要获取的参数的指定类型。按照指定类型获取当前参数,返回这个指定类型的值,然后把 ap 的位置指向变参表中下一个变量的位置;

4)va_end:释放指针,将输入的参数 ap 置为 NULL。通常va_start和va_end是成对出现。

利用malloc()动态分配内存

是否应该强制转换malloc()的返回值类型?

ANSI C之前,C语言没有void *,所以malloc()的返回值为char *,需要强制转换ANSI C之后,malloc()返回值改为了void *,不需要转换C++中,无法将void *赋值给普通指针变量,需要强制转换

这小节关于内存的介绍以及下一小节对齐的介绍还是较为清晰的,要是不了解相关知识的可以去看看,暂时不做描述。

语法揭秘解读C语言声明

C语言阅读起来有时候会反直觉,答案很简单:C语言原本是在美国诞生的语言,所以我们应该用英文来读,可以遵循以下规则:

先看标识符(变量名或函数名)从贴近标识符的地方开始,按如下优先级解释派生类型(指针、数组、函数)用于整合声明的括号用于表示数组的[]、表示函数的()表示指针的*

完成对派生类型的解释之后,通过of、to或returning连接句子添加类型修饰符(位于左侧,比如int、double)C语言英语表达int hoge;hoge is intint hoge[10];hoge is array of intint hoge[10][3];hoge is array of array of intint *hoge[10];hoge is array of pointer to intdouble (*hoge)[3];hoge is pointer to array of doubleint func(int a);func is funciton returning intint (*func_p)(int a) ;funcc_p is pointer to function returning int

英文表达清晰多了,也不容易有歧义。

C语言数据类型的模型

指针:

数组:

数组和指针都是派生类型,单独都较为清晰,混在一起呢,比如指向数组的指针

一听到“指向数组的指针”,有人也许要说: 这不是很简单嘛,数组名后不加[],不就是“指向数组的指针”吗? 抱有这个想法的人,请将 前文重新阅读一下!

的确,在表达式中,数组可以被解读成指针。但是,这不是“指向数组的指针”,而是“指向数组初始元素的指针”

int main() { int (*array_p)[3]; int array[3]; array_p = &array; // array_p = array; // warning: assignment to 'int (*)[3]' from incompatible pointer type 'int *' [-Wincompatible-pointer-types] return 0; }

但注意到,这只是warning,没有强制报错,从地址的角度来看,array 和&array也许就是指向同一地址。但要说起它们的不同之处,那就是它们在做指针运算时结果不同。

因为int 类型的长度是4个字节,所以给“指向 int 的 指针”加1,指针前进4个字节。

但对于“指向 int 的数组(元素个数 3)的指针”,这个指针指向的类型为“int 的数组(元素个数 3)”,当前数组的尺寸为12个字节(如果 int 的长度为 4 个字节),因此给这个指针加1,指针就前进12 个字节。

C语言不存在多维数组

int hoge[3][2]的读法是什么:hoge ia array of array,是数组的数组,有多维数组么,多维是便于逻辑上理解的概念,实际排布如下:

如果函数的形参需要是所谓的“多维数组”,该怎么声明函数呢?如下都是可以的,要记住,函数会被解读成指针,上文提到:在声明函数形参时,才可以将数组的声明解读为指针,也只有这种情况下int a[]和int *a具有相同的意义了。

void func(int (*hoge)[2]); void func(int hoge[3][2]); void func(int hoge[][2]);函数类型的派生

函数类型也是一种派生类型,“参数(类型)”是它的属性。

下文原封不动来自书本,特地使用引用,最好理解这段话。

可是,函数类型和其他派生类型有不太相同的一面。无论是int还是double,亦或数组、指针、结构体,只要是函数以外的类型,大体都可以作为变量被定义。而且,这些变量在内存占用一定的空间。 因此,通过sizeof运算符可以取得它们的大小。 像这样,有特定长度的类型,在标准中称为对象类型。可是,函数类型不是对象类型。因为函数没有特定长度。 所以C中不存在“函数类型的变量”(其实也没有必要存在)。数组类型就是将几个派生类型排列而成的类型。因此,数组类型的全体长度为: 派生源的类型的大小×数组的元素个数。可是,函数类型是无法得到特定长度的,所以从函数类型派生出数组类型是不可能的。也就是说,不可能出现“函数的数组”这样的类型。可以有“指向函数的指针”类型,但不幸的是,对指向函数类型的指针不能做指针运算,因为我们无法得到当前指针类型的大小。此外,函数类型也不能成为结构体和共用体的成员。总而言之:从函数类型是不能派生出除了指针类型之外的其他任何类型的。不过“指向函数的指针类型”,可以组合成指针或者作为结构体、共用体的的数据类型的成员。毕竟“指向函数的指针类型”也是指针类型,而指针类型又是对象类型。 另外,函数类型也不可以从数组类型派生。表达式

基本表达式:

标识符(变量名、函数名)常量(包括整数常量和浮点数常量)字符串常量(使用“”括起来的字符串)使用()括起来的表示式

表达式代表某处的内存区域的时候,我们称当前的表达式为左值(lvalue);相对的是,表达式只是代表值的时候,我们称当前的表达式为右值。

在表达式中,数组会被解读为指针,除了以下三个情况:

当作为sizeof操作数时,返回的是数组整体的长度作为&运算符操作数时,这个上文有例子,指向数组的指针初始化数组时的字符串字面量。我们都知道字符串常量是“char 的数组”,在表达式中它通常被解读成“指向char 的指针”。其实,初始化char的数组时的字符串常量,作为在花括号 中将字符用逗号分开的初始化表达式的省略形式

数组被解读为指针时,该指针不是左值,所以以下代码是错误的:

char str[10]; str = "abc";

只能对左值赋值。

解读C语言声明(续)

const 是在ANSI C中追加的修饰符,它将类型修饰为“只读”。

char *my_strcpy(char *dest, const char *src) { src = NULL; // ←即使对 src 赋值,编译器也没有报错 } // 此时,成为只读的不是 src,而是 src 所指向的对象。 char *my_strcpy(char *dest, const char *src) { *src = 'a'; // ←ERROR!! } ​ // 如果将 src 自身定义为只读,需要写成下面这样: char *my_strcpy(char *dest, char * const src) { src = NULL; // ←ERROR!! } ​ // 如果将 src 和 src 指向的对象都定义为只读,可以写成下面这样: char *my_strcpy(char *dest, const char * const src) { src = NULL; // ←ERROR!! *src = 'a'; // ←ERROR!! } 字符串字面量

用""包裹的字符串称为字符串字面量,类型时char的数组,保存在只读区域,在表达式中,会被解读为指向char的指针。

可是,char 数组的初始化是个例外。此时的字符串常量,作为在花括号中分开书写的初始化表达式的省略形式,编译器会进行特殊处理。

char str[] = "abc"; char str[] = {'a', 'b', 'c', '\0'};

从ANSI C开始,即使是自动变量的数组,也可以被整合来进行初始化。

关于指向函数的指针引发的混论

对于 C 语言,表达式中的函数可以被解读成 “指向函数的指针”。

/*如果发生 SIGSEGV(Segmentation falut),回调函数 segv_handler */ signal(SIGSEGV, segv_handler); signal(SIGSEGV, &segv_handler); // 尽然也是对的 func_p(); (*func_p)(); // 也是对的

上述代码看了之后很迷茫,都解读成指针了,怎么是否取地址,解引用都一样呢?

为了照顾到这种混乱,ANSI C 标准对语法做了以下例外的规定:

表达式中的函数自动转换成“指向函数的指针”。但是,当函数是地址运算符&或者sizeof运算符的操作数时,表达式中的函数不能变换成 “指向函数的指针”函数调用运算符()的操作数不是“函数”,而是“函数的指针”

如果对“指向函数的指针”使用解引用*,它暂时会成为函数,但是因为在表达式中,所以它会被瞬间地变回成“指向函数的指针”。

结论就是,即使对“指向函数的指针”使用*运算符,也是对牛弹琴,因 为此时的运算符*发挥不了任何作用。

阅读完书上这段话的时候我就觉的一言难尽……毕竟C语言历史较久,包容一下。

解读复杂声明int atexit(void (*func)(void)); /* * atexit is function * para is func * returning int * func is pointer to function returning void */ void (*signal(int sig, void (*func)(int)))(int); /* * signal function, para is sig and func * sig is int * func is pointer to function, para is int returning void * signal function returning pointer to function which para is int returning void */

我的感觉是就别翻译成中文了,直接用英文梳理比较方便。

请记住:数组与指针截然不同大家都说C语言的指针比较难,可是真正地让初学者“挠墙”的,并不是指针自身的使用,而是“混淆了数组和指针”。此外,很多“坑爹”的入门书对指针和数组的讲解也是极其混乱。数组和指针的常见用法

这一章节主要是代码上的例子,翻阅即可。

遇到了一个还是容易有歧义的例子。

double (*p[5])[2];

因为[]的优先级比*高,所以一上来就是:p is an array!

读法如下:

p is an arrayeach element is a pointer to array of doubledouble (*p[5])[2]; double element[2] = {1.0, 2.0}; p[0] = &element; printf("%lf\n", (*p[0])[1]);数据结构

第五章主要是指针在数据结构上的应用。

拾遗指定初始化typedef struct { int a; double b; int array[10]; char *str; char array2[4]; } Hoge; ​ void printfHoge(Hoge hoge) { printf("hoge a:%d\n", hoge.a); printf("hoge b:%lf\n", hoge.b); printf("hoge array[4]:%d\n", hoge.array[4]); printf("hoge array[5]:%d\n", hoge.array[5]); printf("hoge str:%s\n", hoge.str); printf("hoge array2:%s\n", hoge.array2); return; } ​ int main() { Hoge hoge = { .a = 1, .b = 2.0, .array = {[4] = 5, 3}, .str = "hello", .array2 = {'b', 'y', 'e', '\0'} }; printfHoge(hoge); Hoge hoge2 = (Hoge){ .a = 1, .b = 2.0, .array = {[4] = 5, 3}, .str = "hello", .array2 = {'b', 'y', 'e', '\0'} }; printfHoge(hoge2); return 0; }

直接对结构体的元素指定值并初始化,或者传递一个结构体字面量都是可以的。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3