c/c++基础零散补充

一、C语言的指针与数组、结构体里的成员数组和指针、传入传出参数、回调函数、头文件包含

指针的本质都只是一个内存地址,如果是多字节变量,则是其内存首地址(低地址),但指针的类型决定了如pa++此类的表达式跨越的内存字节数以及通过pa可以访问到的内存大小。

指针之间的比较运算比的是地址,C语言正是这样规定的,不过C语言的规定更为严谨,只有指向同一个数组中元素的指针之间相互比较才有意义,否则没有意义。那么两个指针相减表示什么? 指针相减表示两个指针之间相差的元素个数,同样只有指向同一个数组中元素的指针之间相减才有意义。两个指针相加表示什么?想不出来它能有什么意义,因此C语言也规定两个指针不能相加。

When two pointers are subtracted, both must point to elements of the same array object or just one past the last element of the array object (C Standard, 6.5.6 [ISO/IEC 9899:2011]); the result is the difference of the subscripts of the two array elements. Otherwise, the operation is undefined behavior (see undefined behavior 48).

Similarly, comparing pointers using the relational operators <<=>=, and > gives the positions of the pointers relative to each other. Subtracting or comparing pointers that do not refer to the same array is undefined behavior (see undefined behavior 48 and undefined behavior 53).

"If P points to the last member of array,then P+1 compares higher than P,even though P+1 points outside the array.Otherwise,pointer comparison is undefined".

Comparing two pointers to distinct members of the same struct object is allowed. Pointers to structure members declared later in the structure compare greater than pointers to members declared earlier in the structure.

Comparing pointers using the equality operators == and != has well-defined semantics regardless of whether or not either of the pointers is null, points into the same object, or points one past the last element of an array object or function.

All pointers to members of the same union object compare equal.

1、指针类型总结

a) int a; // An integer

b) int *a; // A pointer to an integer

c) int **a; // A pointer to a pointer to an integer

d) int a[10]; // An array of 10 integers

e) int *a[10]; // An array of 10 pointers to integers

f) int (*a)[10]; // A pointer to an array of 10 integers

g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer

h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer 

2、指针与数组

数组类型做右值使用时,自动转换成指向数组首元素的指针, 这也解释了为什么数组类型不能相互赋值或初始化, 编译器报的错是error: incompatible types in assignment 但做左值仍然表示整个数组的存储空间,而不是首元素的存储空间,数组名做左值还有一点特殊之处, 不支持++、赋值这些运算符,但支持取地址运算符&,所以&arr是合法的.

示例代码:

int main(void)
{

    char a[4][3][2] = {{{'a', 'b'}, {'c', 'd'}, {'e', 'f'}},
        {{'g', 'h'}, {'i', 'j'}, {'k', 'l'}},
        {{'m', 'n'}, {'o', 'p'}, {'q', 'r'}},
        {{'s', 't'}, {'u', 'v'}, {'w', 'x'}}
    };
    char (*pa)[2] = &a[1][0];
    char (*ppa)[3][2] = &a[1];
    /* a[1][0]是一个数组,有两个元素,在&a[1][0]这个表达式中,数组名做左值,取整个数组
     * 的首地址赋给指针pa.注意,&a[1][0][0] 表示数组a[1][0]的首元素的首地址,而&a[1][0]表示数组a[1][0]的首地址,
     * 显然这两个地址的数值相同,但这两个表达式的类型是两种不同的指针类型,
     * 前者的类型是int *,而后者的类型是int (*)[2] ,指针的本质都只是一个内存地址,但指针的类型决定了如pa++此类的表达式跨越的内存字节数
     * 以及通过pa可以访问到的内存大小
     */

    char r1 = (*(pa + 5))[1];
    /* pa+5 跳过10个字节指向a[2][2],即{'q','r'},(*(pa+5))就表示数组a[2][2]
     * 所以a[2][2][1]可以用(*(pa+5))[1]来表示
     */
    char r2 = pa[5][1]; // (*(pa+5)) 即 pa[5]
    char r3 = (*(ppa + 1))[2][1];
    char r4 = ppa[1][2][1];

    printf("%c %c\n", pa[0][0], ppa[0][0][1]);
    /*  *pa 就表示pa 所指向的数组a[1][0],所以取数组的a[1][0][0] 元素可以用表达式(*pa)[0] 。
     *  注意到*pa 可以写成pa[0] ,所以(*pa)[0] 这个表达式也可以改写成pa[0][0]
     */
    printf("%c %c %c %c\n", r1, r2, r3, r4);

    return 0;
}

一个多维数组在语义上并不等价于一个指向其元素类型的指针,相反它等价于一个“指向数组的指针”。

例如,对于   int a[m][n]来说,a是一个指向一维数组的指针,a+1就是指向第二行的指针,该一维数组有n个元素,故下面的4种表达方法是等价的。:

a[i][j];

*(a[i]+j);

*(*(a+i)+j);

(*(a+i))[j];

假如我们的a的地址是:0Xbfe2e100, 而且是32位机,那么这个程序会输出什么?

#include <stdio.h>
int main(void)
{
    int a[5];
    printf("%x\n", a); //0Xbfe2e100
    printf("%x\n", a+1); // 0Xbfe2e104
    printf("%x\n", &a); // 0Xbfe2e100
    printf("%x\n", &a+1); //0Xbfe2e114
}

3、结构体里的成员数组和指针

struct test{
    int i;
    short c;
    char *p;
    char s[10]; //char s[0] 同理
};

int main(void)
{
    struct test *pt=NULL;
    // 下面打印相对地址,分别为0,4,8,c
    printf("&i = %x\n", &pt->i); //因为操作符优先级,我没有写成&(pt->i)
    printf("&c = %x\n", &pt->c);
    printf("&p = %x\n", &pt->p);
    printf("&s = %x\n", pt->s); //等价于 printf("%x\n", &(pt->s) );
    // crash, 此时结构体尚未存在,何况成员p
    // printf("the str is %s\n", pt->p); 

    return 0;
}

不管结构体的实例是什么——访问其成员其实就是加成员的偏移量;

访问成员数组名其实得到的是数组的相对地址,而访问成员指针其实是相对地址里的内容;

4、传入与传出参数、回调函数

(1)、如果函数接口有指针参数,既可以把指针所指向的数据传给函数使用(称为传入参数);也可以由函数填充指针所指的内存空间,传回给调用者使用(称为传出参数);既是传入参数又是传出参数,称为 Value-result 参数。

(2)、两层指针也是指针,同样可以表示传入参数、传出参数或者 Value-result 参数,只不过该参数所指的内存空间应该解释成一个指针变量。两层指针作为传出参数有一种特别的用法,可以在函数中分配内存,调用者通过传出参数取得指向该内存的指针,如下:void alloc_unit(unit_t **pp); 注意,一层指针的函数接口 void alloc_unit(unit_t *p);不能在函数内分配内存,因为传入指针也是按值传递。但是通过返回指针是可以分配内存的,如 unit_t *alloc_unit(void);

(3)、如果参数是一个函数指针,调用者可以传递一个函数的地址给实现者,让实现者去调用它,这称为回调函数( Callback Function),如 void func(void (*f)(void *), void *p);

5、重复包含头文件有以下问题:

(1). 一是使预处理的速度变慢了,要处理很多本来不需要处理的头文件。

(2). 二是如果有foo.h包含bar.h,bar.h又包含foo.h的情况,预处理器就陷入死循环了

(其实编译器都会规定一个包含层数的上限)。

(3). 三是头文件里有些代码不允许重复出现,虽然变量和函数允许多次声明(只要不是多次定义就行),但头文件里有些代码是不

允许多次出现的,比如 typedef 类型定义和结构体Tag 定义等,在一个程序文件中只允许出现一次。

二、浮点数在计算机内存中的表示

浮点数在计算机中的表示是基于科学计数法(Scientific Notation)的,我们知道32767这个数用科学计数法可以写成3.2767×10^4,3.2767称为尾数(Mantissa,或者叫Significand),4称为指数(Exponent)。浮点数在计算机中的表示与此类似,只不过基数(Radix)是2而不是10。下面我们用一个简单的模型来解释浮点数的基本概念。我们的模型由三部分组成:符号位、指数部分(表示2的多少次方)和尾数部分(小数点前面是0,尾数部分只表示小数点后的数字)。

如果要表示17这个数,我们知道17=17.0×10^0=0.17×10^2,类似地,17=(10001)2×2^0=(0.10001)2×2^5,把尾数的有效数字全部移到小数点后,这样就可以表示为:

如果我们要表示0.25就遇到新的困难了,因为0.25=1×2^(-2)=(0.1)2×2^(-1),而我们的模型中指数部分没有规定如何表示负数。我们可以在指数部分规定一个符号位,然而更广泛采用的办法是使用偏移的指数(Biased Exponent)。规定一个偏移值,比如16,实际的指数要加上这个偏移值再填写到指数部分,这样比16大的就表示正指数,比16小的就表示负指数。要表示0.25,指数部分应该填16-1=15:

现在还有一个问题需要解决:每个浮点数的表示都不唯一,例如17=(0.10001)2×2^5=(0.010001)2×2^6,这样给计算机处理增加了复杂性。为了解决这个问题,我们规定尾数部分的最高位必须是1,也就是说尾数必须以0.1开头,对指数做相应的调整,这称为正规化(Normalize)。由于尾数部分的最高位必须是1,这个1就不必保存了,可以节省出一位来用于提高精度,我们说最高位的1是隐含的(Implied)。这样17就只有一种表示方法了,指数部分应该是16+5=21=(10101)2,尾数部分去掉最高位的1是0001:

三、类型转换

1、Integer Promotion

在一个表达式中,凡是可以使用int或unsigned int类型做右值的地方也都可以使用有符号或无符号的char型、short型和Bit-field。如果原始类型的取值范围都能用int型表示,则其类型被提升为int,如果原始类型的取值范围用int型表示不了,则提升为unsigned int型,这称为IntegerPromotion。做Integer Promotion只影响上述几种类型的值,对其它类型无影响。此外,相应的实参如果是float型的也要被提升为double型,这条规则称为Default Argument Promotion。

2、Usual Arithmetic Conversion

两个算术类型的操作数做算术运算,比如a + b,如果两边操作数的类型不同,编译器会自动做类型转换,使两边类型相同之后才做运算,这称为Usual Arithmetic Conversion。

3、由赋值产生的类型转换

如果赋值或初始化时等号两边的类型不相同,则编译器会把等号右边的类型转换成等号左边的类型再做赋值。

4、强制类型转换

以上三种情况通称为隐式类型转换(Implicit Conversion,或者叫Coercion),编译器根据它自己的一套规则将一种类型自动转换成另一种类型。除此之外,程序员也可以通过类型转换运算符(Cast Operator)自己规定某个表达式要转换成何种类型,这称为显式类型转换(ExplicitConversion)或强制类型转换(Type Cast)。

#include <stdio.h>
int main(int argc, char *argv[])
{
    unsigned short a = -1;
    short b = a;
    printf("%d  %d",a,b);

    return 0;
}
//结果:65535  -1

C语言中常量整数 -1的补码表示为0xFFFFFFFF,截取后面16位FFFF赋值给 变量a(unsigned short),此时 a = 0xFFFF(最高位不是符号位)。

 a又将0xFFFF,直接赋值给short b,此时 b = 0xFFFF(最高位是符号位)。

执行printf("%d %d",a,b);的时候,要将 a和b的值先转换为int型:

 a没有符号所以转为int型为0x0000FFFF(补码);

 b有符号转换为int型为0xFFFFFFFF(补码);

十进制输出值 65535  -1(原码)。

*  unsigned 类型转换为 signed/unsigned类型的时候是直接复制到低位,如果位数不够则高位补0。 * signed类型转换为unsigned/signed类型的时候,也是将补码直接复制到低位,如果位数不够在高位补1还是补0取决于原来的符号位,这称为符号扩展(Sign Extension)。

例1:

char a = 0xe0; unsigned int b = a; 

b 为 0xffffffe0

例2:下面的代码输出的结果是什么,并简单分析结果。

#include <stdio.h>

//无符号数与有符号数相加

int main(int argc, char **argv)
{
    unsigned int a = 6;
    int b = -12;
    if(a+b > 0)
    { 
        printf("dsds\n");
        printf("a+b=%d\n" , a+b);
        printf("a+b=%p\n" , (void *)(a+b));
    }
    else
    {
        printf("ssss\n");
        printf("a+b=%d\n" , a+b);
        printf("a+b=%p\n" , (void *)(a+b));
    }
    return 0;
}

输出:

dsds a+b=-6 a+b=0xFFFFFFFA

首先int 和unsigned int 相加,int先提升为unsigned int,b=-12的内存补码表示为0xfffffff4

故unsigned int c = b,c=0xfffffff4;  c+a = 0xfffffffa, 故c+a >0

但以%d打印时需要转成int,int d = 0xfffffffa,即原码为0x80000006, 也就是 -6

四、函数原型

foobar1(void)和foobar2()是有不同原型的的。我们可以在《ISO/IEC 9899》的C语言规范找到下面两段关于函数声明的描述

10.The special case of an unnamed parameter of type void as the only item in the list specifies that the function has no parameters

14.An identifier list declares only the identifiers of the parameters of the function. An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters. The empty list in a function declarator that is not part of a definition of that function specifies that no information about the number or types of the parameters is supplied.124)

上面两段话的意思就是:foobar1(void)是没有参数,而foobar2()等于foobar2(…)等于参数类型未知。如果我们这样调用 foobar2(33, ch); 是可以编译通过的。

五、整型溢出

对于整型溢出,分为无符号整型溢出和有符号整型溢出。

对于unsigned整型溢出,C的规范是有定义的——“溢出后的数会以2^(8*sizeof(type))作模运算”,也就是说,如果一个unsigned char(1字符,8bits)溢出了,会把溢出的值与256求模。例如:

unsigned char x = 0xff;
printf("%d\n", ++x);

上面的代码会输出:0 (因为0xff + 1是256,与2^8求模后就是0)

对于signed整型的溢出,C的规范定义是“undefined behavior”,也就是说,编译器爱怎么实现就怎么实现。对于大多数编译器来说,算得啥就是啥。比如:

signed char x =0x7f; //注:0xff就是-1了,因为最高位是1也就是负数了
printf("%d\n", ++x);

上面的代码会输出:-128,因为0x7f + 0×01得到0×80,也就是二进制的1000 0000,符号位为1,负数,后面为全0,就是负的最小数,即-128。

参考:

《linux c 编程一站式学习》

coolshell.cn

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员Gank

【译】RxJava变换操作符:-concatMap(-)与-flatMap(-)的比较

是时候回归写作了。(译者注:原作者吧啦吧啦唠家常,这里就不做翻译了,但是,有两个重要的链接,点我,再点我)

872
来自专栏Python爱好者

Java基础笔记02

792
来自专栏编程

Go中defer的5 个坑-第一部分

首发于:https://studygolang.com/articles/12061 Go 中 defer 的 5 个坑 - 第一部分 通过本节的学习以避免掉入...

2165
来自专栏后端技术探索

浅谈PHP5中垃圾回收算法(Garbage Collection)的演化

PHP是一门托管型语言,在PHP编程中程序员不需要手工处理内存资源的分配与释放(使用C编写PHP或Zend扩展除外),这就意味着PHP本身实现了垃圾回收机制(G...

551
来自专栏机器学习算法与Python学习

python基础语法(1)

从今天起,将进行python的一个系列学习,从基本的语法学起,后期会推出一些关于web开发,网络爬虫以及用python的第三方库进行数据挖掘与机器学习等高级的开...

35214
来自专栏互联网杂技

StackOverflow上关于JavsScript的热门问答

Q1:javascript的闭包是如何工作的? 正如爱因斯坦所说的: 如果你不能把它解释给一个六岁的小孩,说明你对它还不够了解。 我曾尝试向一个27岁的朋友解释...

3176
来自专栏java一日一条

Java 枚举查找并不抛异常的实现

Java Enum是一个非常有用的功能,但很多人通常并不能充分利用,这是因为一些库不会优先择用该功能。通常我们也可以正确使用Java枚举功能,但在许多代码库中往...

723
来自专栏Coding迪斯尼

reactjs自制Monkey语言编译器:解析组合表达式,ifelse语句块和间套函数调用

443
来自专栏Play & Scala 技术分享

为Play初学者准备的Scala基础知识

3156
来自专栏IT可乐

Java数据结构和算法(六)——前缀、中缀、后缀表达式

  前面我们介绍了三种数据结构,第一种数组主要用作数据存储,但是后面的两种栈和队列我们说主要作为程序功能实现的辅助工具,其中在介绍栈时我们知道栈可以用来做单词逆...

1969

扫码关注云+社区