几个C语言知识点

重新整理以前的笔记,基本来自《C语言深度解剖》。

原码/反码/补码

原码:原码就是这个数本身的二进制形式,其中最高位为符号位。
补码:在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。
反码:正数的反码与其原码相同;负数的反码是对其原码逐位取反,但符号位除外。

求补码的方法:整数或零,则补码等于原码,负数则补码等于除符号位外,各位取反加一
求反码的方法:正数,反码与原码相同,负数,对其原码逐位取反,但符号位除外,符号位还是一

在计算机系统中,数值一律用补码来表示。

问题1:

1
2
3
4
5
6
7
8
9
10
11
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}

以上代码输出为255

问题2:
char a = -300,求 int a
300的原码:1 0010 1100
取反:1101 0011
加一:1101 0100
300的补码:1101 0100
符号位:1
求原码:101 0100
取反:010 1011
加一:010 1100
求得:-44

问题3:
int i = -20;
unsigned j = 10;
i+j的值为多少?为什么?

问题4:
下面的代码有什么问题?

1
2
3
4
5
unsigned i ;
for (i=9;i>=0;i--)
{
printf("%u\n",i);
}

数据类型

  • 基本类型 - 数值类型、字符类型
  • 构造类型 - 数组、结构体、共用体、枚举类型
  • 指针类型
  • 空类型

常用数据类型在32位和64位CPU上的字节数比较:
测试程序:

1
2
3
4
5
printf("char[%d], char*[%d], short int[%d], int[%d], unsigned int[%d], float[%d], double[%d], long[%d], long long[%d], unsigned long[%d]\n",
sizeof(char),sizeof(char *),
sizeof(short int),sizeof(int),sizeof(unsigned int),
sizeof(float),sizeof(double),
sizeof(long),sizeof(long long),sizeof(unsigned long));

不同CPU类型的输出结果:
x86_64:

char[1], char*[8], short int[2], int[4], unsigned int[4], float[4], double[8], long[8], long long[8], unsigned long[8]

i686:

char[1], char*[4], short int[2], int[4], unsigned int[4], float[4], double[8], long[4], long long[8], unsigned long[4]

总结:很明显的比较结果,指针和长整形由4个字节升为8个字节

32个关键字

举例:
register - 声明寄存器变量
const - 声明只读变量
volatile - 说明变量在程序执行中可被隐含地改变。这个关键字声明的变量,编译器对访问该变>量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

分析:
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。(对Java来说:Java内存模型告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。)

下面是volatile变量的几个例子:

  1. 并行设备的硬件寄存器(如:状态寄存器)
  2. 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
  3. 多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所用这些都要求volatile变量。不懂得volatile内容将会带来灾难。 假设被面试者正确地回答了这是问题(嗯,怀疑这否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。

有如下3个问题:

  1. 一个参数既可以是const还可以是volatile吗?解释为什么。
  2. 一个指针可以是volatile 吗?解释为什么。
  3. 下面的函数有什么错误:
    1
    2
    3
    4
    int square(volatile int *ptr) 
    {
    return *ptr * *ptr;
    }

下面是答案:

  1. 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
  2. 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。
  3. 这段代码的有个恶作剧。这段代码的目的是用来返指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:
    1
    2
    3
    4
    5
    6
    int square(volatile int *ptr) 
    {
    int a,b; a = *ptr;
    b = *ptr;
    return a * b;
    }
    由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
    1
    2
    3
    4
    5
    long square(volatile int *ptr) 
    {
    int a; a = *ptr;
    return a * a;
    }

extern: 说明变量是在其他文件中声明(也可以看作是引用变量)
sizeof: 计算对象所占内存空间大小

函数参数的声明举例:void func(int i,char c)
定义声明最重要的区别:定义创建了对象,并为这个对象分配了内存,声明没有分配内存

编译器在默认的缺省情况下,所有变量都是auto的。

register变量必须是能被CPU寄存器所接受的类型。意味着register变量必须是一个单个的值,并且其长度应小于或等于整形的长度。而且register变量可能不存在内存中,所以不能用取址运算符“&”来获取register变量的地址。比如说,32位的CPU,最多是4个字节,整形和长整形都是占4个字节的数据类型。
注意:register变量只是对编译器的建议,编译器并不一定会将该变量作为register变量。

static关键字作用

  1. 修饰变量
    静态区是分配静态变量、全局变量空间的;初始化的全局变量放在数据段;局部变量放在栈;未初始化的全局变量放在bss段。
    从C程序运行的角度看,内存几大部分:静态存储区、堆区和栈区。
    静态存储区:内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存在静态数据、全局数据和常量。
    全局区(静态区)(static) —- 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include<stdio.h>
    int a = 0; //全局初始化区
    char *p1; //全局未初始化区
    void main(void)
    {
    int b; //栈
    char s[] = "abc"; //栈
    char *p2; //栈
    char *p3 = "123456"; //123456\0在常量区,p3在栈上
    static int c = 0; //全局(静态)初始化区
    p1 = (char *)malloc(10); //堆
    p2 = (char *)malloc(20); //堆
    p1 = "123456"; //123456\0在常量区,编译器将p1与p3所指向的“123456\0”优化成同一个地方
    }

    全局变量、局部变量、静态全局变量、静态局部变量的区别,如下:

    • 从作用域看
    1. 全局变量具有全局域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包括全局变量定义的源文件需要用extern关键字再次声明这个全局变量。
    2. 静态局部变量具有局部作用域。它只被初始化一次,自从第一次初始化直到程序结束都一直存在。它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
    3. 局部变量也只有局部作用域,他是自动对象,他在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用结束后,变量就被撤销,其所占用的内存也被收回。
    4. 静态全局变量(其他文件即使使用extern声明也没法使用它)也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,他作用于定义它的文件里,不能作用到其他文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同的静态全局变量,它们也是不同的变量。
    • 从分配内存空间看
      全局变量、静态全局变量、静态局部变量都在静态存储区分配空间,而局部变量在栈分配空间。

    把局部变量改变为静态变量后是改变了它的存储方式,即改变了它的生命周期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围,因此static这个说明符在不同的地方起的作用是不同的。

    注意:
    对于静态局部变量的理解:

    1. 若全局变量仅在单个函数中使用,则可以将这个变量修改为改函数的静态局部变量
    2. 由于被static修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值
      直观的例子:
      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
      int k,ii = 0;
      static int j;
      void func1(void)
      {
      static int i = 0;
      i++;
      ii++;
      printf("i = %d\n",i);
      printf("ii = %d\n",ii);
      }
      void func2(void)
      {
      j = 0;
      j++;
      printf("j = %d\n",j);
      }
      int main()
      {
      for(k=0;k<10;k++)
      {
      func1();
      func2();
      sleep(1);
      }
      return 0;
      }
  1. 修饰函数
    函数前加static使得函数成为静态函数。但此处“static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称为内部函数)。
  1. C++里对static赋予了第三个作用(如果成员声明为static,可以在外部直接访问)

    1. 静态数据成员
  • 静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一个内存,供所有对象共用
  • 静态数据成员存储在全局数据区。静态数据成员定义时要分配空间,所以不能在类声明中定义
    1. 静态成员函数
  • 静态成员之间可以相互访问,包括静态成员函数访问静态数据成员和访问静态成员函数
  • 非静态成员函数可以任意地访问静态成员函数和静态数据成员
  • 静态成员函数不能访问非静态成员函数(不包括构造函数)和非静态数据成员

sizeof关键字

int i = 0;
A). sizeof(int);
B). sizeof(i);
C). sizeof int;
D). sizeof i;
A、B和D都正确,C错误。

sizeof(int) *p表示什么意思?
32位系统下:
int *p = NULL;
sizeof(p)的值是多少? 4
sizeof(*p)呢? 4
char *q = NULL;
sizeof(q)的值是多少? 4
sizeof(*q)呢? 1
int a[100];
sizeof (a) 的值是多少? 400
sizeof(a[100])呢? 4
sizeof(&a)呢? 4
sizeof(&a[0])呢? 4
int b[100];
void fun(int b[100])
{
sizeof(b);// sizeof (b) 的值是多少?是个指针类型,把 int b[100] 改为 char b[100] 也是输出 4
}

与零值比较

  1. bool变量
    bool bTestFlag = FALSE;//为什么一般初始化为FALSE比较好?
    FALSE在编译器里被定义为0,而TRUE则不一定是1,不同的编译器有不同的定义。

  2. float变量
    float fTestVal = 0.0;
    const float EPSINON = 0.00001;
    if((fTestVal >= -EPSIONON) && (fTestVal <= EPSINON))

return关键字

return可以返回些什么东西?如:

1
2
3
4
5
6
char * Func(void)
{
char str[30];
...
return str;
}

str属于局部变量,位于栈内存中,在Func结束的时候被释放,所以返回str会导致错误。
规则:return语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。

const关键字

  1. const修饰的是只读变量,而不是常量

  2. 节省空间,避免不必要的内存分配,同时提高效率
    例如:

    1
    2
    3
    4
    5
    6
    7
    #define M 3 //宏常量
    const int N = 5; //此时并未将 N 放入内存中
    ...
    int i = N; //此时为 N 分配内存,以后不再分配
    int I = M; //预编译期间进行宏替换,分配内存
    int j = N; //没有内存分配
    int J = M; //再进行宏替换,又一次分配内存

    const定义的只读变量从汇编的角度看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份拷贝(因为它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。#define宏没有类型,而const修饰的只读变量具有特定的类型。

注意:

  • 当const修饰指针时
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const int *p; // p 可变,p 指向的对象不可变
    int const *p; // p 可变,p 指向的对象不可变
    int *const p; // p 不可变,p 指向的对象可变
    const int *const p; // 指针 p 和 p 指向的对象都不可变
    const int a;
    int const a;
    const int *a;
    int * const a;
    int const * a const;
    //前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。
  • const修饰函数返回值(返回指针)
    如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针。
    例如函数:
    1
    const char * GetString(void);
    如下语句将出现编译错误:
    1
    char *str = GetString();
    正确的用法是:
    1
    const char *str = GetString();
  • const修饰的只读变量不能用来作为定义数组的维数,也不能放在case关键字后面。
  • 常量指针与指针常量的区别
    *(指针)和const(常量)谁在前先读谁;* 象征着地址,const 象征着内容;谁在前面谁就不允许改变。
    1
    2
    3
    4
    5
    6
    7
    int a = 3;
    int b = 1;
    int c = 2;
    int const *p1 = &b; // const 在前,定义为常量指针
    int *const p2 = &c; // * 在前,定义为指针常量
    //常量指针p1: 指向的地址可以变,但内容不可以重新赋值,内容的改变只能通过修改地址指向后变换。
    //指针常量p2: 指向的地址不可以重新赋值,但内容可以改变,必须初始化,地址跟随一生。
    问:C语言函数返回值为 const 型 有什么意义?
    答:当为指针时,有意义,一般数值没有意义 当返回为const指针时,表示对返回指针所指向的数据内容不要进行修改。有修改则程序会报错。
  • C语言可以通过指针修改const修饰的只读变量
    1
    2
    3
    4
    const int a = 20;
    int *p = (int *) &a;
    *p = 100;
    printf("%d %d\n", *p, a);

结构体

  1. 空结构体占一个字节

  2. 柔性数组

    1
    2
    3
    4
    5
    6
    typedef struct test
    {
    int a;
    double b;
    char c[0];
    }

    柔性数组允许在定义结构体的时候创建一个空数组,而这个数组的大小可以在程序运行的过程中根据你的需求进行更改
    这个空数组必须声明为结构体的最后一个成员,并且还要求这样的结构体至少包含一个其他类型的成员
    柔性数组不占用内存
    给结构体分配内存:

    1
    2
    test *stpTest = (test *)malloc(sizeof(test)+100*sizeof(char)); //分配100
    test *stpTest = malloc(sizeof(*test) + sizeof(char) * strlen(paddress) + 1); //动态分配
  3. 在 C++里 struct 关键字与 class 关键字一般可以通用,只有一个很小的区别。struct 的成员默认情况下属性是 public 的,而 class 成员却是 private 的。

  4. union维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间,在union中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。

  5. 大端模式( Big_endian):字数据的高字节存储在低地址中,而字数据的低字节则存放 在高地址中。 小端模式( Little_endian):字数据的高字节存储在高地址中,而字数据的低字节则存放 在低地址中。
    判断系统大小端的方法 – 可以利用union类型数据的特点:所有成员的起始地址一致

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int checkSystem()
    {
    union check
    {
    int i;
    char ch;
    }c;
    c.i = 1;
    return (c.ch == 1);
    }
    int main()
    {
    checkSystem() == 1 ? printf("Little-endian/n") : printf("Big-endian/n");
    return 0;
    }

    在 x86 系统下,输出的值为多少? // x86 - 小端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    int main()
    {
    int a[5] = {1,2,3,4,5};
    int *prt1 = (int *)(&a + 1);
    int *ptr2 = (int *)((int)a + 1);
    printf("%d, %x\n", ptr1[-1], *ptr2);
    return 0;
    }
    // 答案: 5,2000000
    // 解释:
    // a 是数组首元素的首地址,而 &a 是整个数组的首地址,a+1 是偏移 int 个字节, 而 &a+1 是偏移整个数组的大小
    // 所以,此题 &a+1 偏移 20 个字节,ptr1[-1] = 5
    // 数组 a 的分布是:{01 00 00 00, 02 00 00 00, 03 00 00 00, 04 00 00 00, 05 00 00 00},地址从左到右递增
    // (int)a + 1 是向后移动了 1 个字节,即 00 00 00 02,由于是小端,所以 02 00 00 00

#define用法

1
2
3
4
#define PCHAR char*
PCHAR p3,p4; // 等效于 char * p3,p4;
#define PCHAR (char*)
PCHAR p3,p4; // 等效于 (char *) p3,p4;
1
2
3
4
5
6
#define X 3
#define Y X*2
#undef X
#define X 2
int z = Y;
// z 的值是多少? 4

除法与取余运算

参与运算的量均为整形时,结果为整形,舍去小数。如果运算量中有一个为实型,结果为双精度实型。
例如:5/2=2, 1/2=0, 5/2.0=2.5
参与运算的量均为整形,求余运算的结果等于两个数相除后的余数。
例如:5%2=1, 1%2=1

5%2.0和5.0%2的结果是语法错误

除号的正负取舍和一般的算数一样,符号相同为正,相异为负,求余符号的正负取舍和被除数符号相同。
-3/16=0, 16/-3=-5, -3%16=-3, 16%-3=1

printf(“%d\n”,10/2.5);
整形数据默认为int,实型数据默认为double。
整形只能使用整形说明符 %d 等。
实型只能使用实型说明符。

#pragma预处理

  • #pragma message(“消息文本”)
    当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。
    假设我们希望判断自己有没有在源代码的什么地方定义了_X86 这个宏可以用下面的方法
    1
    2
    3
    #ifdef _X86
    #pragma message(“_X86 macro activated!”)
    #endif

内存对齐

性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
32位CPU与内存模型.png

举例:

1
2
3
4
5
6
7
struct test
{
char x1;
short x2;
float x3;
char x4;
}

由于编译器默认情况下会对这个struct作自然边界对齐,结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然边界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大边界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

使用指令#pragma pack (n),编译器将按照 n 个字节对齐。
使用指令#pragma pack (),编译器将取消自定义字节对齐方式。
在#pragma pack (n)和#pragma pack ()之间的代码按 n 个字节对齐。

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma pack(8)
struct TestStruct4
{
char a;
long b;
};
struct TestStruct5
{
char c;
TestStruct4 d;
long long e;
};
#pragma pack ()

问题:

  1. sizeof(TestStruct5)等于多少?
  2. TestStruct5 的 c 后面空了几个字节接着是 d?

TestStruct5 中,c 和 TestStruct4 中的 a 一样,按 1 字节对齐,而 d 是个结构,它是 8 个字节,它按什么对齐呢?
对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个, TestStruct4 的就是 4 。
(复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度)

TestStruct4的内存布局:

a b
1*** 1111

TestStruct5的内存布局:

c a b e
1*** 1*** 1111**** 11111111

答案:

  1. 24
  2. 3

注意:
对于数组,比如:char a[3];它的对齐方式和分别写 3 个 char 是一样的.也就是说,它还是按 1 个字节对齐.如果写: typedef char Array3[3];Array3 这种类型的对齐方式还是按 1 个字节对齐,而不是按它的长度。

补充:联合体内存对齐的问题

1
2
3
4
5
6
7
8
//例1:
union U
{
char s[9];
int n;
double d;
}u;
//sizeof(u) = 16
1
2
3
4
5
6
7
8
9
10
//例2:
union student
{
char name[20];
double a1;
char sex;
int age;
float height;
}stu;
//sizeof(stu) = 24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//例3:
void main()
{
union
{
long a;
struct
{
char a1;
short a2;
char a3;
}st_a;
}un_a;
printf("%d, %d\n", (int)(sizeof(un_a.st_a)), (int)(sizeof(un_a)));
return;
}
//sizeof(un_a.st_a) = 6
//sizeof(un_a) = 8

从这里可以看出联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:

  1. 大小足够容纳最宽的成员
  2. 大小能被其包含的所有基本数据类型的大小所整除

a[i]

对于数组a[i]
数组首元素的地址:a、&a[0]
数组的地址:&a

指针和数组的定义与声明

  1. 编译器会把存在指针变量中的任何数据当做地址来处理

  2. 定义为数组,声明为指针
    文件1:char a[100]; //定义了数组a
    文件2:extern char *a; //声明它为指针 //这样做是错的

    解析:extern char a[] 与 extern char a[100] 等价的原因,因为这是声明,不分配空间,所以编译器无需知道这个数组有多少个元素。这两个声明都告诉编译器 a 是在别的文件中被定义的一个数组, a 同时代表数组 a 的首元素的首地址,也就是这块内存的起始地址。数组内任何元素的地址都只需要知道这个地址就可以计算出来。但是,当你声明为 extern char *a 时,编译器理所当然的认为 a 是一个指针变量,在 32 位系统下,占 4 个 byte。这 4 个 byte 里保存了一个地址,这个地址上存的是字符类型数据。虽然在文件 1 中,编译器知道 a 是一个数组,但是在文件 2 中,编译器并不知道这点。大多数编译器是按文件分别编译的,编译器只按照本文件中声明的类型来处理。所以,虽然 a 实际大小为 100 个 byte,但是在文件 2 中,编译器认为 a 只占 4 个 byte。

  3. 定义为指针,声明为数组
    文件1:char *p = “abcdefg”;
    文件2:extern char p[]; //这样的做法也是错误的

    解析:在文件 1 中, 编译器分配 4 个 byte 空间, 并命名为 p。 同时 p 里保存了字符串常量 “ abcdefg” 的首字符的首地址。这个字符串常量本身保存在内存的静态区,其内容不可更改。在文件 2 中,编译器认为 p 是一个数组,其大小为 4 个 byte,数组内保存的是 char 类型的数据。

地址的强制转换

1
2
3
4
5
6
7
8
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;

假设 p 的值为 0x100000,如下表达式的值分别为多少?

  1. p + 0x1 = 0x____ ?
  2. (unsigned long) p + 0x1 = 0x____ ?
  3. (unsigned int *) p + 0x1 = 0x____ ?

指针变量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。

  1. p + 0x1 的值为 0x100000 + sizof( Test) * 0x1 ,此结构体的大小为 20byte,所以 p + 0x1 的值为: 0x100014
  2. 将指针变量 p 保存的值强制转换成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以这个表达式其实就是一个无符号的长整型数加上另一个整数。所以其值为: 0x100001
  3. p 被强制转换成一个指向无符号整型的指针。所以其值为: 0x100000 + sizeof( unsigned int) * 0x1,等于 0x100004

二维数组

例子:

1
2
3
4
int a[5][5];
int (*p)[4];
p = a;
//问&p[4][2] - &a[4][2]的值为多少?
  1. 前面我们讲过,当数组名 a 作为右值时,代表的是数组首元素的首地址。这里的 a 为二维数组,我们把数组 a 看作是包含 5 个 int 类型元素的一维数组,里面再存储了一个一维数组。如此,则 a 在这里代表的是 a[0]的首地址。 a+1 表示的是一维数组 a 的第二个元素。 a[4]表示的是一维数组 a 的第 5 个元素,而这个元素里又存了一个一维数组。所以&a[4][2]表示的是&a[0][0]+4*5*sizeof(int) + 2*sizeof(int)。
  2. 根据定义, p 是指向一个包含 4 个元素的数组的指针。也就是说 p+1 表示的是指针 p 向后移动了一个“包含 4 个 int 类型元素的数组”。这里 1 的单位是 p 所指向的空间,即4*sizeof(int)。所以, p[4]相对于 p[0]来说是向后移动了 4 个“包含 4 个 int 类型元素的数组”,即&p[4]表示的是&p[0]+4*4*sizeof(int)。由于 p 被初始化为&a[0],那么&p[4][2]表示的是&a[0][0]+4*4*sizeof(int)+2* sizeof(int)。
  3. 其实我们最简单的办法就是画内存布局图

二级指针

一维数组参数

C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。
同样的,函数的返回值也不能是一个数组,而只能是指针。

一级指针参数

问: 能否把指针变量本身传递给一个函数?
例子:

1
2
3
4
5
6
7
8
9
10
void func(char *p)
{
char c = p[3]; //或者是 char c = *(p+3)
}
int main()
{
char *p2 = "abcdefg";
fun(p2);
return 0;
}

答: 无法把指针变量本身传递给一个函数。
func函数实际运行时,对 p2 做一份拷贝,假设其拷贝名为_p2。那传递到func函数内部的就是_p2 而并非 p2 本身。

再看如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
void GetMemory (char * p, int num)
{
p = (char *)malloc(num*sizeof(char));
}
int main()
{
char *str = NULL;
GetMemory(str,10);
strcpy(str,"hello");
free(str); //free 并没有起作用,内存泄漏
return 0;
}

在运行 strcpy(str,”hello”)语句的时候发生错误。这时候观察 str 的值, 发现仍然为 NULL。也就是说 str 本身并没有改变,我们 malloc 的内存的地址并没有赋给str,而是赋给了_str。而这个_str 是编译器自动分配和回收的,我们根本就无法使用。

解决方法:

  1. 用return
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    char * GetMemory (char * p, int num)
    {
    p = (char *)malloc(num*sizeof(char));
    return p;
    }
    int main()
    {
    char *str = NULL;
    str = GetMemory(str,10);
    strcpy(str,"hello");
    free(str);
    return 0;
    }
  2. 用二级指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void GetMemory (char ** p, int num)
    {
    *p = (char *)malloc(num*sizeof(char));
    }
    int main()
    {
    char *str = NULL;
    GetMemory(&str,10);
    strcpy(str,"hello");
    free(str);
    return 0;
    }
    //注意,这里的参数是&str 而非 str。这样的话传递过去的是 str 的地址,是一个值。在函数内部,用钥匙("*")来开锁: *(&str),其值就是 str。所以 malloc 分配的内存地址是真正赋值给了 str 本身。

二维数组参数与二维指针参数

Page 100

函数指针使用

函数指针数组

函数指针数组的指针

函数

  1. 如果参数是指针,且仅作为输入参数用,则应在类型前加const,以防止该指针在函数体内被意外修改。例如:
    1
    void str_copy (char *strDestination, const char *strSource);
  2. return语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。例如:
    1
    2
    3
    4
    5
    6
    char * Func(void)
    {
    char str[30];
    ...
    return str;
    }
  3. 在函数体的“入口处”,对参数的有效性进行检查。尤其是指针参数,尽量使用assert宏做入口校验,而不使用if语句校验。

文件结构

防止头文件被重复包含

1
2
3
#ifndef __FN_FILENAME_H__
#define __FN_FILENAME_H__
#endif

其中“FN_FILENAME”一般为本头文件名大写,这样可以有效避免重复,因为同一个工程中不可能存在两个同名的头文件。

常见的内存错误及对策

  1. 指针没有指向一块合法的内存

    1. 结构体成员指针未初始化(解决方法是malloc一块空间)
    2. 没有为结构体指针分配足够的内存
    3. 函数的入口校验
      • 不管什么时候,我们使用指针之前一定要确保指针是有效的
      • 一般在函数入口处使用 assert(NULL != p)对参数进行校验。在非参数的地方使用 if(NULL != p)来校验。但这都有一个要求,即 p 在定义的同时被初始化为 NULL 了
      • assert 是一个宏,而不是函数,包含在 assert.h 头文件中。如果其后面括号里的值为假,则程序终止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码
  2. 为指针分配的内存太小

  3. 内存分配成功,但并未初始化

  4. 内存越界

  5. 内存泄漏
    会产生泄漏的内存就是堆上的内存(这里不讨论资源或句柄等泄漏情况),也就是说由 malloc 系列函数或 new 操作符分配的内存。如果用完之后没有及时 free 或 delete,这块内存就无法释放,直到整个程序终止

    • malloc 函数的原型:(void *)malloc(int size)
      使用 malloc 函数同样要注意这点:如果所申请的内存块大于目前堆上剩余内存块(整块),则内存分配会失败,函数返回 NULL
      既然 malloc 函数申请内存有不成功的可能,那我们在使用指向这块内存的指针时,必须用 if(NULL != p)语句来验证内存确实分配成功了

    • 内存释放 free函数:free(p)
      指针变量 p 本身保存的地址并没有改变,但是它对这个地址处的那块内存却已经没有所有权了。那块被释放的内存里面保存的值也没有改变,只是再也没有办法使用了
      如果对 p 连续两次以上使用 free 函数,肯定会发生错误。因为第一使用 free 函数时, p 所属的内存已经被释放,第二次使用时已经无内存可释放了
      既然使用 free 函数之后指针变量 p 本身保存的地址并没有改变, 那我们就需要重新把 p的值变为 NULL:p = NULL
      释放完块内存之后,没有把指针置 NULL,这个指针就成为了“野指针”,这是很危险的,而且也是经常出错的地方。所以一定要记住一条:free 完之后,一定要给指针置 NULL

其他典型题目

  • 当unsigned类型与signed类型运算时,默认转换成unsigned类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    int main()
    {
    int a = -6;
    unsigned int b = 4;
    if(a+b > 0)
    printf("a+b>0\n");//这句话被打印
    else
    printf("a+b<0\n");
    int z = a+b;
    if(z > 0)
    printf("z>0");
    else
    printf("z<0");//这句话被打印
    }

    当int和unsigned int相加时,要将int转化为unsigned int,而int小于0,所以它的最高位是符号位,为1,所以转化的结果是一个很大的正数,在第一个if语句中,是两个“正数”相加,结果自然就大于0了。而在z = a+b这一句时,它把a+b的结果看做一个int类型,而a+b最高位为1,所以z是一个负数,所以打印的是第二个语句。

  • 在以下数组定义中,正确的有(AD)
    A) int a[‘a’];
    B) int a[3.4];
    C) int a[][4];
    D) int *a[10];
    int a[][4]; 不明确的话,就不知道分配多少内存。

    在定义二维数组的时候对其进行初始化,也可以省略第一维,编译器会根据你的初始化语句自动决定第一维度。(有初始化的时候,第二维的数字代表分配内存的长度,第一维的数字代表分配内存倍数,倍数可以让机器去数,但长度没有的话就不好开辟空间了)

    多维数组声明时,可以省略第一维,但是不能省略第二维或者更高维的大小。这是由编译器原理限制的。
    设有数组int a[m][n],如果要访问a[i][j]的值,编译器的寻址方式为:

    1
    &a[i][j]=&a[0][0]+i*sizeof(int)*n+j*sizeof(int); //注意n为第二维的维数

    因此,可以省略第一维的维数,不能省略其他维的维数。

  • printf压栈
    例1:

    1
    2
    3
    int n = 5;
    printf("%d %d %d %d\n",++n,++n,n++,++n); // 9 9 6 9
    printf("n = %d\n",n); // 9

    例2:

    1
    2
    int i = 5;
    printf("%d %d %d %d\n", i, --i, i, i--);

    “对于a++的结果,是有ebp寻址函数栈空间来记录中间结果的,在最后给printf压栈的时候,再从栈中把中间结果取出来;而对于++a的结果,则直接压寄存器变量,寄存器经过了所有的自增操作。”

  • 数组和指针

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    char str1[] = "abc";
    char str2[] = "abc";

    const char str3[] = "abc";
    const char str4[] = "abc";

    const char *str5 = "abc";
    const char *str6 = "abc";

    char *str7 = "abc";
    char *str8 = "abc";

    cout << ( str1 == str2 ) << endl;
    cout << ( str3 == str4 ) << endl;
    cout << ( str5 == str6 ) << endl;
    cout << ( str7 == str8 ) << endl;

    结果是:0 0 1 1
    解答:str1,str2,str3,str4是数组变量,它们有各自的内存空间。而str5,str6,str7,str8是指针,它们指向相同的常量区域。

  • 字符串常量

    1
    2
    3
    4
    char* s="AAA";
    printf("%s",s);
    s[0]='B';
    printf("%s",s);

    有什么错?
    “AAA”是字符串常量。s是指针,指向这个字符串常量,所以声明s的时候就有问题。
    cosnt char* s=”AAA”;
    然后又因为是常量,所以对是s[0]的赋值操作是不合法的。

  • 优先级
    () > [] > *

    指针数组:int *(ap[])
    数组指针:int (*ap) []
    所以 int * ap[]是指针数组,因为 [] 的优先级高于 *

    指针函数:int * ( fp() ) // () 优先级高,先与 fp 结合成一个函数,再由 int * 说明这是一个整形的指针函数
    函数指针:int (*fp) () // () 运算符,自左至右,首先说明 fp 是一个指针,指向一个返回值是整形的函数
    所以 int *fp() 是指针函数,因为 () 的优先级高于 *

  • 定义

  1. 变量的定义中,除了 变量名 以外的内容就是该变量的类型
    int a; a 变量名, int 类型
    int b[10]; b 变量名 ,int [10] 数组的类型
    int * p p 变量名 int * 指针的类型
  2. 数组的定义中,除了 数组名[元素数目] 以外的内容就是该数组的元素类型
    int a[10] int 元素类型 整形数组
    int a [3][5] int[5] 数组类型 二维数组
    int * p[5] int * 指针类型 指针数组
  3. 指针的定义中,除了 * 指针变量名,以外的内容就是该指针指向对象的类型
    int *p; int 整形 整形指针
    int (*p)[5]; int[5] 数组 数组指针
    int (*p) (); int () 返回值为int类型的函数 函数指针
  • 函数指针和指针函数
    函数指针的本质是一个变量,该变量的内容指向一个函数。
    指针函数的本质是一个函数,只不过其返回值是一个指针类型的变量。

  • 地址
    要对绝对地址0x100000赋值,我们可以用

    1
    (unsigned int*)0x100000 = 1234;

    那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?

    1
    *((void (*)( ))0x100000 ) ( );

    首先要将0x100000强制转换成函数指针,即:

    1
    (void (*)())0x100000

    然后再调用它:

    1
    *((void (*)())0x100000)();

    用typedef可以看得更直观些:

    1
    2
    typedef void(*)() voidFuncPtr;
    *((voidFuncPtr)0x100000)();
  • 为什么有垃圾值?
    因为操作系统释放内存只是释放对这个内存地址的使用权限(回收内存空间),下一个变量使用这个内存地址的时候,内存地址上保存的值还是上一次遗留下来的值,这就是垃圾值,因此变量需要初始化。

  • 如果两指针属于同一数组,则可以相减

  • 一个字节一个地址。所有的指针变量只占4个字节,用第一个字节的地址表示整个变量的地址。