iOS开发系列--C语言之基础知识

概览

当前移动开发的趋势已经势不可挡,这个系列希望浅谈一下个人对IOS开发的一些见解,这个IOS系列计划从几个角度去说IOS开发:

  • C语言
  • OC基础
  • IOS开发(iphone/ipad)
  • Swift

这么看下去还有大量的内容需要持续补充,但是今天我们从最基础的C语言开始,C语言部分我将分成几个章节去说,今天我们简单看一下C的一些基础知识,更高级的内容我将放到后面的文章中。

今天基础知识分为以下几点内容(注意:循环、条件语句在此不再赘述):

  1. Hello World
  2. 运行过程
  3. 数据类型
  4. 运算符
  5. 常用函数

iOS开发系列--C语言之数组和字符串

概览

数组在C语言中有着特殊的地位,它有很多特性,例如它的存储是连续的,数组的名称就是数组的地址等。而在C语言中是没有String类型的,那么如果要表示一个字符串,就必须使用字符数组。今天主要就介绍如下三个方面:

  1. 一维数组
  2. 多维数组
  3. 字符串

iOS开发系列--C语言之指针

概览

指针是C语言的精髓,但是很多初学者往往对于指针的概念并不深刻,以至于学完之后随着时间的推移越来越模糊,感觉指针难以掌握,本文通过简单的例子试图将指针解释清楚,今天的重点有几个方面:

  1. 什么是指针
  2. 数组和指针
  3. 函数指针

iOS开发系列--C语言之预处理

概述

大家都知道一个C程序的运行包括编译和链接两个阶段,其实在编译之前预处理器首先要进行预处理操作,将处理完产生的一个新的源文件进行编译。由于预处理指令是在编译之前就进行了,因此很多时候它要比在程序运行时进行操作效率高。在C语言中包括三类预处理指令,今天将一一介绍:

  1. 宏定义
  2. 条件编译
  3. 文件包含

iOS开发系列--C语言之存储方式和作用域

概述

基本上每种语言都要讨论这个话题,C语言也不例外,因为只有你完全了解每个变量或函数存储方式、作用范围和销毁时间才可能正确的使用这门语言。今天将着重介绍C语言中变量作用范围、存储方式、生命周期、作用域和可访问性。

  1. 变量作用范围
  2. 存储方式
  3. 可访问性

iOS开发系列--C语言之构造类型

概述

在第一节中我们就提到C语言的构造类型,分为:数组、结构体、枚举、共用体,当然前面数组的内容已经说了很多了,这一节将会重点说一下其他三种类型。

  1. 结构体
  2. 枚举
  3. 共用体

Hello World

既然是IOS开发系列首先看一下在Mac OS X中的C的运行

打开Xcode

选择命令行程序

填写项目名称并选择使用C语言

选择保存目录

自动生成如下代码

OK,在Xcode上我们编写自己的程序如下

//
//  main.c
//  C语言基础
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#include <stdio.h>

void showMessage(){
    printf("Hello,World!\n");
}

int main(int argc, const char * argv[]) {
    showMessage();
    return 0;
}

在上面的程序中我们需要解释几点:

  1. main函数是程序入口,一个程序只能有一个main()函数,需要有一个整型返回值(事实上返回值int可以省略,但是这并不代表不返回值,而是默认为int;我们也可以在main()函数中不提供return,这是因为c语言语法要求不够严格);
  2. #include是预处理指令,用于包含指定文件(注意在编译前即处理),它实际做的工作就是把对应文件复制到指定的位置; 包含的内容可以是任何类型的文件,而不仅仅是.h文件;
  3. 上面的showMessage函数必须写在main()函数上面,如果写在下面则必须在main()函数之前声明;

注意:#include 包含文件时有两种方式:使用<>和””。区别就是<>包含只会查找编译器库函数文件,因此适用于包含库函数;而“”包含则首先查找程序当前目录,如果没有找到则查找库函数路径,因此适用于自定义文件;

运行过程

C语言的运行分为两大步:编译和链接

  • 编译:编译阶段会将对应的xxx.c源文件(ASCII格式)编译成目标文件xxx.obj,它是二进制格式(当然一般我们会有多个.c文件,也就会生成多个对应的.obj);在编译之前要进行预处理(例如#include指令),在编译的同时还要进行语法检查;生成的.obj文件并不能单独执行,因为各个.obj之间是有关联的,而且他们还各自引用了C语言库函数;
  • 链接:链接的过程就是将各个.obj文件和C语言库函数一起组合生成一个可执行文件的过程;

扩展

在大型项目开发中程序中所有的代码都写到一个文件中是不现实的,我们通常将一个子操作分为两个文件:.c文件和.h文件。在.c文件中实现对应的函数,在.h中进行函数声明,这样只要在主函数上方包含对应的头文件就可以将子操作分离出来而且不用考虑顺序问题。例如改写“Hello World”的例子(注意message对应的.c和.h文件名完全可以不相同,但是出于规范的目的我们还是取相同的文件名):

message.h

//
//  Message.h
//  C语言基础
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//
void showMessage();

message.c

//
//  Message.c
//  C语言基础
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#include <stdio.h>

void showMessage(){
    printf("Hello,World!\n");
}

main.c

//
//  main.c
//  C语言基础
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#include <stdio.h>
#include "Message.h"

int main(int argc, const char * argv[]) {
    showMessage();
    return 0;
}

可以发现程序仍然可以正常运行,但是我们思考一个问题:如果我们不分成两个文件,直接在主函数文件中包含message.c是否也可以正常运行呢?答案是否定的,原因是由于编译生成的两个文件main.obj和 message.obj在链接时会发现main.obj中已经有message.obj中定义的showMessage函数,抛出“标示符重复”的错误。

数据类型

类型修饰符

从上图我们可以清晰的看到C语言的数据类型结构,当然对于这些类型我们还有一些类型修饰符(或叫限定符)

  • short 短型 ,修饰int、double
  • long 长型,修饰int、double
  • signed 有符号型,修饰int、char
  • unsigned 无符号型,修饰int、char

对于类型修饰符需要做如下解释

  1. 这些修饰符经常用来修饰int型,在修饰int类型时int可以省略;
  2. short和long会改变int型的长度,不同编译器项长度不相同,但是short长度不大于int,int长度不大于long;
  3. signed、unsigned不改变类型长度,仅仅表示最高位是否为符号位,unsigned表示大于等于0的正数;

当然有时候我们必须清楚每个类型占用的字节,下表列出常用数据类型占用的存储空间

注意:char类型是最小的数据类型单位,在任何类型的编译器下都是占用1个字节,char类型的变量赋值可以直接赋值等于某个字符也可以赋值为整数(对应的ASCII值);可以使用两个long来修饰一个整形(就是经常使用的8字节的整形long long),但是两个long不能修饰double而且也不存在两个short,否则编译警告;一个浮点型常量如果后面加上f编译器认为它是float类型,否则认为double类型,例如10.0是double类型,10.0f是float类型。

运算符

C语言中有34中运算符,同C#、Java等语言没有太大的区别,这里指列出一些注意事项

  1. 关系运算符为真就返回1,为假就返回0;在条件语言中非0即真(负数、正数均为真),只有0为假 ;
  2. C语言可以不保存关系运算符的值 ;
  3. 逗号表达式最终的值是最后一个表达式的值;

针对上面几点看以下例子

//
//  main.c
//  C语言基础
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#include <stdio.h>

int main(int argc, const char * argv[]) {
    int a=2>1,b=2<1,c=99,d=0;
    int f=0,g=0,h=0,e=(f=3,g=4,h=5);

    a>0;//没有保存运算结果

    printf("%d,%d\n",a,b);//结果:1,0

    if(c){//可以通过
        printf("true.\n");
    }
    if(d){//无法通过
        printf("false\n");
    }

    printf("%d\n",e);//结果:5
    return 0;
}

常用函数

printf()函数

printf()函数用于向标准输出设备输出数据,配合格式符可以完成强大的输出功能,上面的例子中我们已经使用了这个函数。

通常我们的输出不是固定内容而是包含某些变量,此时需要用到格式符,常用格式符如下

对于格式符的输出宽度和浮点数的小数位我们可以进行精确的控制

//
//  main.c
//  C语言基础
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#include <stdio.h>

int main(int argc, const char * argv[]) {
    int a=16;
    float b=79.3f;
    printf("[a=%4d]\n",a);
    printf("[a=%-4d]\n",a);
    printf("[b=%10f]\n",b);
    printf("[b=%.2f]\n",b);
    printf("[b=%4.2f]\n",b);
    return 0;
}

运行结果如下

从运行结果我们不难发现格式符%前的正数可以设置前端补齐,负数设置后端对齐,如果数据的总长度超过设置的修饰长度,则按照实际长度显示;小数点后的整数用于控制小数点后保留小数位的长度。

scanf()函数

scanf()函数用于从标准输入设备接收输入数据

//
//  main.c
//  C语言基础
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 cmjstudio. All rights reserved.
//

#include <stdio.h>

int main(int argc, const char * argv[]) {
    int a,b,c;
    scanf("%d,%d,%d",&a,&b,&c);//此时需要输入:1,2,3 然后回车
    printf("a=%d,b=%d,c=%d\n",a,b,c);
    return 0;
}

对于scanf()函数我们需求强调几点

  1. 参数接收以回车进行结束操作
  2. 如果需要接收多个参数,多个参数之间的分隔符是任意的,但是如果分隔符是“空格”则实际输入的时候分隔符可以使空格、tab和回车(最后一个回车认为是结束符)

6.循环语句

1)for循环

c语言中的for循环语句使用最为灵活,不仅可以用于循环次数已经确定的情况,而且可以用于循环次数不确定而只给出循环结束条件的情况,它完全可以代替while语句.
for(表达式 1;表达式 2;表达式 3)语句
它的执行过程如下:
(1)先求表达式 1.
(2)求表达式2,若其值为真(值为非0),则执行for语句中指定的内嵌语句,然后执行下面第三步 做若为

假(值为0),则结束循环,转到第5步.
(3)求解表达式3
(4)转回上面第(2)步骤继续执行;
(5)结束循环,执行for语句下面的一个语句;
for(循环变量赋初值;循环条件;循环变量增值)语句

for(i=1;i<=100;i++)sum=sum+i;

它的执行相当于

i=1; 
while(i<=100){ 
sum=sum+i; 
i++; 
}

显然,用for语句更简单、方便。
说明:
(1)for语句的一般形式中的"表达式1"可以省略,此时应在for语句之前给循环变量赋初值.注意省略表达式1时,其后的分号不能省略.如for(;i<=100;i++){....};
(2)如果表达式2省略 即不判断循环条件,循环无终止地循环下去,也就是认为表达式2始终为真.
例如:for(i=1;;i++){.....};
相当于

i=1; 
while(1) 
{sum=sum+1; 
i++; 
}

(3)表达式3也可以省略,但此时程序设计者应另外设法保证循环能正常结束.如:

for(i=1;i<=100;) 
{sum=sum+1; 
i++; 
}

这个例子的循环增量没有放在表达式三的位置 而是作为循环体的一部分 其效果是一样的.
(4)可以省略表达式1表达式3,只有表达式2 即只给循环条件.

for(;i<=100;) 

sum=sum+i; 
i++; 

这个相当于 
whlie(i<=100) 

sum=sum+i; 
i++; 
}

(5)三个表达式都可以省略,如:
for(;;)语句
相当于
while(1)语句
即不设初值 不判断条件(认为表达式2为真值)循环变量不增值,无终止的执行循环体.
(6)表达式1也可以是设置循环变量初值的赋值表达式,也可以是与循环变量无关的其他表达式.如:

for(sum=0;i<=100;i++) 

sum=sum+i; 

for(sum=0,i=0;i<=100;i++) 

sum=sum+i; 
}

2)while循环:


#include <stdio.h>

int main(void)
{
    int i=0;
    while (i<10) {
        i++;
        printf("%d\n", i);
    }
    getchar();
    return 0;
}

3.) do while 循环:


#include <stdio.h>

int main(void)
{
    int i=0;
    do
    {
        i++;
        printf("%d\n", i);             
    } while (i<10);
    getchar();
    return 0;
}

3.1 while 与 do while 的区别:


#include <stdio.h>

int main(void)
{
    int i=10;

    while (i<10)
    {
        printf("while");    //这个不会执行
    }
     
    do
    {
        printf("do while"); //这个会执行
    } while (i<10);

    getchar();
    return 0;
}

3.2. break 与 continue:


#include <stdio.h>

int main(void)
{
    int i=0;

    while (i<10)
    {
        i++;
        if (i == 8) break;      /* 不超过 8 */
        if (i%2 == 0) continue; /* 只要单数 */
        printf("%d\n", i);
    }

    getchar();
    return 0;
}

3.3 无限循环:


#include <stdio.h>

int main(void)
{
    int i=0;
    while (1)    //或 while (!0)
    {
        i++;
        printf("%d\n", i);
        if (i == 100) break;
    }
    getchar();
    return 0;
}

7.条件语句

1)if语句

1. 常规:


#include <stdio.h>

int main(void)
{
    int i;

    for (i = 0; i < 10; i++) {
        if (i%2 == 0) printf("%d 是偶数\n", i);
        if (i%2 != 0) printf("%d 是奇数\n", i);
    }
    getchar();
    return 0;
}

#include <stdio.h>

int main(void)
{
    int i;
    for (i = 0; i < 10; i++) {
        if (i > 4)
            printf("%d\n", i);
        else
            printf("*\n");
    }
    getchar();
    return 0;
}

2. && 与 ||


#include <stdio.h>

int main(void)
{
    int i;
    for (i = 0; i < 10; i++) {
        if (i>3 && i<7) {
            printf("%d\n", i);
        }
    }
    getchar();
    return 0;
}

#include <stdio.h>

int main(void)
{
    int i;
    for (i = 0; i < 10; i++) {
        if (i<3 || i>7) {
            printf("%d\n", i);
        }
    }
    getchar();
    return 0;
}

3. & 与 |


#include <stdio.h>

int main(void)
{
    int i;
    for (i = 0; i < 10; i++) {
        if (i>3 & i<7) {
            printf("%d\n", i);
        }
    }
    getchar();
    return 0;
}

#include <stdio.h>

int main(void)
{
    int i;
    for (i = 0; i < 10; i++) {
        if (i<3 | i>7) {
            printf("%d\n", i);
        }
    }
    getchar();
    return 0;
}

4. !


#include <stdio.h>

int main(void)
{
    int i;
    for (i = 0; i < 10; i++) {
        if (!(i > 4)) {
            printf("%d\n", i);
        }
    }
    getchar();
    return 0;
}

5. 梯次:


#include <stdio.h>

int main(void)
{
    int i;
    for (i = 0; i < 10; i++) {
        if (i/2 == 0) {
            printf("%d: 0-1\n", i);     
        } else if(i/2 == 1) {
            printf("%d: 2-3\n", i);
        } else if(i/2 == 2) {
            printf("%d: 4-5\n", i);
        } else {
            printf("%d: 6-9\n", i);
        }
    }
    getchar();
    return 0;
}

6. 嵌套:


#include <stdio.h>

int main(void)
{
    int i;
    for (i = 0; i < 10; i++) {
        if (i > 2) {
            if (i%2 == 0) {
                printf("%d\n", i);
            }
        }
    }
    getchar();
    return 0;
}

7. 简化的 if 语句(? :)


#include <stdio.h>

int main(void)
{
    int i,j;
 
    for (i = 0; i < 10; i++) {
        j = i<5 ? 1 : 5;  //
        printf("%d\n", j); 
    }
    getchar();
    return 0;
}

#include <stdio.h>

int main(void)
{
    int i,j;
 
    for (i = 0; i < 10; i++) {
        i<5 ? printf("1\n") : printf("5\n");
    }
    getchar();
    return 0;
}

2)switch语句

C语言作为底层开发最常用的语言,要理解C语言的运行机制,阅读对应的汇编代码是非常有帮助的。我会在下篇分析一下汇编中的switch。

这次是上篇,就当作一个热身吧,看看你是否已经了解switch语句是怎么执行的。

阅读下面的代码,请问,从语法上看,有多少处错误?

 1 #include <stdio.h>
 2 #define TWO 2
 3
 4 int main(int argc, char ** argv)
 5 {
 6     switch(argc) {
 7         case 1:
 8             printf("case 1\n");
 9         case TWO:
10             printf("case 2\n");
11         case 3:
12             printf("case3\n");
13         xxx:
14             printf("xxx\n");
15         default:
16             printf("default\n");
17             break;
18         case 4:
19             printf("case 4\n");
20             goto xxx;
21     }
22     return 0;
23 }

好了看完了,是不是有以下的疑惑?

TWO是宏定义,可以写在case后面吗?有的地方为什么没有break?有没有关系?xxx是什么东西?default不是应该放在最后吗?

如果你对这些疑惑都有很清楚的答案,那么你会回答,这段代码从语法上来讲,错误个数是0. 是的,没有错误。

我们可以编译并成功生成a.out

看一下运行的结果。

randy@ubuntu:~/C_Language/switch$ ./a.out
case1
case2
case3
xxx
default<br>
randy@ubuntu:~/C_Language/switch$ ./a.out a
case2
case3
xxx
default<br>
randy@ubuntu:~/C_Language/switch$ ./a.out a b
case3
xxx
default<br>
randy@ubuntu:~/C_Language/switch$ ./a.out a b c
case 4
xxx
default<br>
randy@ubuntu:~/C_Language/switch$ ./a.out a b c d
default

小结:

1.语法,"case 常量表达式: 语句序列”,宏定义的TWO经过预编译被替换成2.

2.switch不会在每个case标签后面的语句执行完毕后自动终止。一旦执行某个case语句,程序将会一次执行后面所有的case,除非遇到break语句。

这被称之为“fall through”。

3.switch内部的任何语句都可以加上标签,所有的case都是可选的,任何形式的语句,包括带标签的语句都是允许的(xxx)。

4.break中断了什么?break语句事实上跳出的是最近的那层循环语句或switch语句。

5.各个case和default顺序可以是任意的,如果没有default,而且每个case选项都不符合,则相当于switch语句没有执行。

-------------------------------------------------------------------------------------------------------------

想要深入地理解语言的运行机理,阅读汇编代码是很有帮助的。

前奏:我们这里用的汇编代码格式是AT&T的,这个微软的intel格式不一样。

AT&T格式是GCC,OBJDUMP等一些其他我们在linux环境下常用工具的默认格式。

今天就一起再来看看switch语句吧。

关键词:跳转,跳转表

先来一个最简单的例子:

 1 int switch_eg(int x, int n)
 2 {
 3     int result = x;
 4
 5     switch (n) {
 6         case 100:
 7             result += 10;
 8             break;
 9         case 102:
10              result -= 10;
11             break;
12         default:
13             result = 0;
14     }
15
16     return result;
17 }

看一下其汇编代码:我会逐条注释。

命令是gcc -O1 -S test2.c

 1     .file    "test2.c"
 2     .text
 3     .globl    switch_eg
 4     .type    switch_eg, @function
 5 switch_eg:
 6 .LFB0:
 7     .cfi_startproc
 8     movl    4(%esp), %ecx                  //x在esp+4的位置,存入寄存器ecx
 9     movl    8(%esp), %edx                  //n在esp+8的位置,存入寄存器edx
10     leal    10(%ecx), %eax                 //将ecx的值+10存入eax,也就是x+10
11     cmpl    $100, %edx                     //将n和100比较
12     je    .L2                              //n==100,跳到L2
13     subl    $10, %ecx                      //n!=100,x=x-10
14     cmpl    $102, %edx                     //n和102比较
15     movl    $0, %eax                       //eax=0
16     cmove    %ecx, %eax                    //如果n==102,eax=ecx
17 .L2:
18     rep                                    //返回,result存在eax
19     ret
20     .cfi_endproc
21 .LFE0:
22     .size    switch_eg, .-switch_eg
23     .ident    "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
24     .section    .note.GNU-stack,"",@progbits

好了,对着注释看,是不是很简单呢。由此我们知道,switch语句实际上也是一种条件语句,而条件语句的核心是跳转。聪明的你应该还会想到跳转的标签个数应该是和case语句的分支个数成正比的。

可是,当case语句分支很多时,岂不是各种jmp?编译器很聪明的使用了一种叫跳转表的方法来解决这个问题。

其实也简单,跳转表的思想就是将要跳转的代码的地址存入一个数组中,然后根据不同的条件跳转到对应的地址处,就像访问数组一样。

空说太枯燥了,还是看个例子吧。(例子来源:深入理解计算机系统3.6.7)

C:

 1 int switch_eg(int x, int n) {
 2     int result = x;
 3     switch (n) {
 4         case 100:
 5             result *= 13;
 6             break;
 7
 8         case 102:
 9             result += 10;
10             /* fall throuth */
11
12         case 103:
13             result += 11;
14             break;
15
16         case 104:
17         case 106:
18             result *= result;
19             break;
20
21         default:
22             result = 0;
23     }
24
25     return result;
26 }

同样的看一下对应的汇编。我省略了一些无关的代码。

 1     movl    4(%esp), %eax
 2     movl    8(%esp), %edx
 3     subl    $100, %edx
 4     cmpl    $6, %edx
 5     ja    .L8
 6     jmp    *.L7(,%edx,4)
 7     .section    .rodata
 8     .align 4
 9     .align 4
10 .L7:
11     .long    .L3
12     .long    .L8
13     .long    .L4
14     .long    .L5
15     .long    .L6
16     .long    .L8
17     .long    .L6
18     .text
19 .L3:
20     leal    (%eax,%eax,2), %edx
21     leal    (%eax,%edx,4), %eax
22     ret
23 .L4:
24     addl    $10, %eax
25 .L5:
26     addl    $11, %eax
27     ret
28 .L6:
29     imull    %eax, %eax
30     ret
31 .L8:
32     movl    $0, %eax
33     ret

解释一下关键点:

首先生成了一张跳转表,以L7为基准,4自己为对齐单位,加上偏移就能跳转到相应的标签。

比如,L7+0就是跳到L3处,L7+4就是跳转到L8处,依次类推。


 7     .section    .rodata
 8     .align 4
 9     .align 410 .L7:
11     .long    .L3
12     .long    .L8
13     .long    .L4
14     .long    .L5
15     .long    .L6
16     .long    .L8
17     .long    .L6

第6行: jmp *.L7(,%edx,4)

表示   goto *jt[index],举个例子,假设现在n是102,edx里面是2(102-100),查表得L7+2*4处,即跳到L4处。

23 .L4:
24     addl    $10, %eax

将eax的值+10,这和C是对应的。

 8         case 102:
 9             result += 10;

注意到L4后面没有ret了,这就是我们上篇所说的fall through规则。不清楚可以看一下上篇的例子C语言拾遗(四):分析switch语句机制---上篇

好了,其他的分支,各位可以自己用其他例子验证一下,看是不是跟C语言代码逻辑是一样的,欢迎讨论。

小结:

swith语句的本质是条件语句,条件语句的本质是跳转。

当case分支多了的时候(一般大于四个时),编译器巧妙地通过跳转表来访问代码位置。

-------------------------------------------------------------------------------------------------------------------------------------------------------------

C语言其他常用知识点

C 语言

  • 运算符
  • 表达式
  • 条件语句
  • 循环语句
  • 转向语句
  • 空语句

示例
1、运算符
cOperator.h

#ifndef _MYHEAD_OPERATOR_
#define _MYHEAD_OPERATOR_ 

#ifdef __cplusplus
extern "C"
#endif  

char *demo_cOperator();

#endif  

cOperator.c

/*
 * 运算符
 */

#include "pch.h"
#include "cOperator.h"
#include "cHelper.h"

char *demo_cOperator()
{
    // 算数运算符:+, -, *, /, %, ++, --
    // 关系运算符:>, <, ==, >=, <=, !=
    // 逻辑运算符:&&, ||, !
    // 位操作运算符:&, |, ~, ^, <<, >>
    //     & - 按位与:都是 1 则为 1,其他情况为 0;比如 1001 & 0001 = 0001
    //     | - 按位或:有一个是 1 则为 1,其他情况为 0;比如 1001 | 0001 = 1001
    //     ^ - 按位异或:不一样则为 1,一样则为 0;比如 1001 | 0001 = 1000
    //     ~ - 按位非:~1001 = 0110
    // 赋值运算符:=, +=, -=, *=, /=, %=, &=, |=, ^=, >>=, <<=
    // 条件运算符:三目运算符 ?:
    // 指针运算符:* 用于取内容   & 用于取地址

    // i++, i本身加1,表达式i++的值为i加1前的值
    // ++i, i本身加1,表达式++i的值为i加1后的值
    int i = 0;
    i++; // 此处 i 的值为 1, i++ 的值为 0
    int j = 0;
    ++j; // 此处 j 的值为 1, ++j 的值为 1

    // sizeof - 是 C 里面的关键字,而不是函数,其是求字节数运算符

    // 计算一个数据类型所占用空间的大小
    int intSize = sizeof(short); // 2, 不同平台下 int short long 之类的占用空间可能不一样,用 sizeof 就可以知道其占用空间的大小了

    char *str = "abcdefghijklmnopqrstuvwxyz";
    int dataSize = sizeof(str); // 4, 这里计算的是 str 指针所占用空间的大小

    return str_concat2(int_toString(intSize), int_toString(dataSize));
}

2、表达式,条件语句,循环语句,转向语句,空语句等
cStatement.h

#ifndef _MYHEAD_STATEMENT_
#define _MYHEAD_STATEMENT_ 

#ifdef __cplusplus
extern "C"
#endif  

char *demo_cStatement();

#endif  

cStatement.c

/*
 * 表达式,条件语句,循环语句,控制语句,空语句等
 */

#include "pch.h"
#include "cStatement.h"
#include "cHelper.h"

char *demo_cStatement()
{
    // 所谓表达式是由运算及运算对象所组成的具有特定含义的式子
    // 所谓表达式语句就是由表达式加上分号“;”组成的
    // i++, i本身加1,表达式i++的值为i加1前的值
    // ++i, i本身加1,表达式++i的值为i加1后的值

    // 复合语句(拿大括号括起来)
    {
        int p1 = 1;
        int p2 = 2;
    }

    // 只有“;”的语句就叫做空语句
    while (0)
    {
        ; // 这一句就是空语句
    }
    // 上面的等于下面的
    while (0)
        ;
    // 上面的等于下面的
    while (0);

    // 分支结构的语句(if else)
    int a = 0, b;

    if (a == 1) b = 1; // 注意:变量 a 在此之前如果不初始化的话,则这里是无法编译过的,也就是说未赋值的变量不能使用

    if (a == 1)
        b = 2;
    else if (a == 2)
        b = 3;
    else
        b = 4;

    // 分支结构的语句(switch case)
    int x = 0, y;
    switch (x)
    {
        case 1:
            y = 2;
            break;
        case 2:
            y = 3;
            break;
        default:
            y = 4;
    }

    // 分支结构的语句(?:)
    int m, n = 0;
    m = n == 1 ? 2 : 3;

    // 循环语句(while)
    int i = 0;
    while (i < 100)
    {
        i++;
    }

    // 循环语句(do while)
    int j = 0;
    do
    {
        j++;
    }
    while (j < 100);

    // 循环语句(for)
    for (i = 0; i < 100; i++)
    {
    }
    for (i = 0; i < 100; )
    {
        i++;
    }
    for (i = 0, j = 0; i < 100 && j < 100; i++, j++)
    {
        i++;
        j++;
    }
    int z = 0;
    for ( ; z < 100; )
    {
        z++;
    }

    // 转向语句:break - 跳出循环
    // 转向语句:continue - 跳过循环体的剩余的语句,直接进入下一次循环判断
    // 转向语句:return - 退出函数,并提供返回值
    // 转向语句:goto - 跳转至指定的标识符处,并执行其后的语句(需合理使用,比如多层嵌套语句退出时,使用 goto 就是很合理的)

    int result = 0;
myAnchor: // 自定义标识符(标识符加冒号)
    if (result == 1)
    {
        result = 100;
    }
    if (result == 0)
    {
        result = 1;
        goto myAnchor; // 跳转至指定的标识符处(myAnchor:),并执行其后的语句
    }

    return str_concat2("看代码及注释吧", int_toString(result));
}

iOS开发系列--C语言之数组和字符串

概览

数组在C语言中有着特殊的地位,它有很多特性,例如它的存储是连续的,数组的名称就是数组的地址等。而在C语言中是没有String类型的,那么如果要表示一个字符串,就必须使用字符数组。今天主要就介绍如下三个方面:

  1. 一维数组
  2. 多维数组
  3. 字符串

一维数组

一维数组操作比较简单,但是需要注意,数组长度必须是固定的,长度不能使用变量进行初始化;如果声明的同时进行赋值则数组长度可以省略,编译器会自动计算数组长度;同时数组不能先声明再一次性赋值(当然可以对每个元素一一赋值)。

#include <stdio.h>

int main(){
    int len = 2;
    //int a[len] = { 1, 2};//错误,不能使变量
    int a[2];//正确
    a[0] = 1;
    a[1] = 2;
    //a[2] = 3;//超过数组长度,但是编译器并不会检查,运行报错
    int b['a'] = {1,2,3};//'a'=97,所以可以作为数组长度,但是后面的元素没有初始化,其值默认为0
    for (int i = 0; i < 97; ++i){
        printf("b[%d]=%d\n",i,b[i]);
    }
    int c[2 * 3];//2*3是固定值可以作为数组长度
    int d[] = { 1, 2, 3 };//如果初始化的同时赋值则数组长度可以省略,当前个数为3
}

扩展--数组的存储

数组在内存中存储在一块连续的空间中,如果知道数组类型(int、float等)和初始地址就可以知道其他元素的地址,同时由于数组名等于数组第一个元素的地址,所以当数组作为参数(作为参数时形参可以省略)其实是引用传递。

#include <stdio.h>

int main(){
    int const l = 3;
    int a[l] = { 1, 2,3 };
    for (int i = 0; i < l; ++i){
        //由于当前在32位编译器下,int型长度为4个字节,可以判断出三个地址两两相差都是4
        printf("a[%d]=%d,address=%x\n", i, a[i], &a[i]);
    }
    /*当前输出结果:
    a[0] = 1, address = c9f95c
    a[1] = 2, address = c9f960
    a[2] = 3, address = c9f964*/
}

我们看一下上面定义的数组在内存中存储结构

再来看一下数组作为参数传递的情况,数组作为参数传递的是数组的地址

#include <stdio.h>

void changeValue(int a[]){ a[0] = 10;
}

int main(){ int a[2] = {1,2};
    changeValue(a); for (int i = 0; i < 2; ++i){
        printf("a[%d]=%d\n",i,a[i]);
    } /*打印结果
    a[0]=10
    a[1]=2
    */
}

多维数组

多维数组其实可以看成是一个特殊的一维数组,只是每个元素又是一个一维数组,下面简单看一下多维数组的初始化和赋值

#include <stdio.h>

int main(){
    int a[2][3];//2行3列,二维数组可以看成是一个特殊的一维数组,只是它的每一个元素又是一个一维数组
    a[0][0] = 1;
    a[0][1] = 2;
    a[0][2] = 3;
    a[1][0] = 4;
    a[1][1] = 5;
    a[1][2] = 6;
    for (int i = 0; i < 2; ++i){
        for (int j = 0; j < 3; ++j){
            printf("a[%d][%d]=%d,address=%x\n", i, j, a[i][j], &a[i][j]);
        }
    }
    /*打印结果
    a[0][0]=1,address=f8fb24
    a[0][1]=2,address=f8fb28
    a[0][2]=3,address=f8fb2c
    a[1][0]=4,address=f8fb30
    a[1][1]=5,address=f8fb34
    a[1][2]=6,address=f8fb38
    */
    //初始化并直接赋值
    int b[2][3] = { { 1, 2, 3 }, { 4, 5, 6 } };
    //由于数组的赋值顺序是先从第一行第一列,再第一行第二列...然后第二行第一列...,所以我们也可以写成如下形式
    int c[2][3] = { 1, 2, 3, 4, 5, 6 };
    //也可以只初始化部分数据,其余元素默认为0
    int d[2][3] = { 1, 2, 3, 4 };
    for (int i = 0; i < 2; ++i){
        for (int j = 0; j < 3; ++j){
            printf("d[%d][%d]=%d\n", i, j, d[i][j]);
        }
    }
    /*打印结果
    d[0][0]=1
    d[0][1]=2
    d[0][2]=3
    d[1][0]=4
    d[1][1]=0
    d[1][2]=0
    */
    //当然下面赋值也可以
    int e[2][3] = { {}, { 4, 5, 6 } };
    //可以省略行号,但是绝对不可以省略列号,因为按照上面说的赋值顺序,它无法判断有多少行
    int f[][3] = { {1,2,3},{4,5,6} };
}

扩展--多维数组的存储

以上面a数组为例,它在内存中的结构如下图

根据上图和一维数组的存储,对于二维数组可以得出如下结论:数组名就是整个二维数组的地址,也等于第一行数组名的地址,还等于第一个元素的地址;第二行数组名等于第二行第一个元素的地址。用表达式表示:

  1. a=a[0]=&a[0][0]
  2. a[1]=&a[1][0]

同样可以得出a[i][j]=a[i]+j。关于三维数组、四维数组等多维数组,其实可以以此类推,在此不再赘述。

字符串

在C语言中是没有字符串类型的,如果要表示字符串需要使用char类型的数组,因为字符串本身就是多个字符的组合。但是需要注意的是字符串是一个特殊的数组,在它的结束位置必须要加一个”\0”(ASCII中0是空操作符,表示什么也不做)来表示字符串结束,否则编译器是不知道什么时候字符串已经结束的。当直接使用字符串赋值的时候程序会自动加上”\0”作为结束符。

//
//  main.c
//  ArrayAndString
//
//  Created by KenshinCui on 14-7-06.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int main(int argc, const char * argv[])
{

    char a[] = {'K','e','n','s','h','i','n','\0'};
    printf("%s",a); //结果:Kenshin,注意使用%s输出字符串内容,如果换成整形输出格式其实输出的是a的地址
    printf("\n");
    printf("address=%x", a); //结果:address=5fbff890
    printf("\n");
    //后面的\0绝对不能省略,如果没有\0则会出现如下情况
    char b[] = { 'I', 'a', 'm'};
    printf("%s",b); //没有按照期望输出,多了一些垃圾数据,在当前环境打印结果:IamKenshin
    printf("\n");
    printf("address=%x",b); //结果:address=5fbff88d
    printf("\n");
    //直接赋值为字符串,此时不需要手动添加\0,编译器会自动添加
    char c[] = "Kenshin";
    printf("c=%s",c); //结果:c=Kenshin
    printf("\n");

    //二维数组存储多个字符串
    char d[2][3]={"Kenshin","Kaoru","Rose","Jack","Tom","Jerry"};

    return 0;
}

从上面代码注释中可以看到打印b的时候不是直接打印出来“Iam”而是打印出了“IamKenshin”,原因就是编译器无法判断字符串是否结束,要解释为什么打印出“IamKenshin”我们需要了解a和b在内存中的存储。

从图中我们不难发现由于a占用8个字节,而定义完a后直接定义了b,此时分配的空间连续,b占用3个字节,这样当输出b的时候由于输出完“Iam”之后并未遇到”\0”标记,程序继续输出直到遇到数组a中的“\0”才结束,因此输出内容为“IamKenshin”。

扩展--字符串操作常用函数

下面简单看一下和字符和字符串相关的常用的几个函数

//
//  main.c
//  ArrayAndString
//
//  Created by Kenshin Cui on 14-7-04.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int main(int argc, const char * argv[])
{
    /*字符操作*/
    putchar('a'); //结果:a,putchar一次只能输出一个字符
    printf("\n");
    putchar(97);//结果:a
    printf("\n");
    char a;
    a=getchar();//getchar()一次只能接收一个字符,可以接收空格、tab、回车
    printf("a=%c",a);
    printf("\n");

    /*字符串操作*/
    char b[]="Kenshin";
    printf("b=%s",b);
    printf("\n");
    puts(b); //puts用于输出单个字符串,不能像printf格式化输出,会自动添加换行
    printf("\n");

    char c[10];
    scanf("%s",c);//注意c没必要写成&c,因为c本身就代表了数组的地址
    printf("c=%s\n",c);//注意即使你输入的内容大于10,也能正确输出,但是下面的gets()函数却不行
    printf("\n");

    //gets()函数,注意它是不安全的,因为接收的时候不知道它的大小容易造成溢出,建议不要使用
    char d[10];
    gets(d); //gets一次只能接收一个字符串,但是scanf可接收多个;scanf不能接收空格、tab,gets则可以
    printf("d=%s",d);
    printf("\n");

    char e[]={'K','s','\0'};
    printf("%lu",strlen(e)); //结果是:2,不是3,因为\0不计入长度
    printf("\n");
    char f[]={"Kenshin"};
    printf("%lu",strlen(f)); //结果是:7
    printf("\n");

    char g[5];
    strcpy(g,"hello,world!");
    printf("%s",g); //结果是:hello,即使定义的g长度为5,但是也能完全拷贝进去
    printf("\n");
    char h[5];
    char i[]={'a','b','c','\0','d','e','f','\0'};
    strcpy(h,i);
    printf("%s",h); //结果是:abc,遇到第一个\0则结束
    printf("\n");

    strcat(i,"ghi");
    printf("%s",i); //结果是:abcghi,注意不是abcdefghi,strcat,从i第一\0开始使用“ghi”覆盖,覆盖完之后加上一个\0,在内存中目前应该是:{'a','b','c','g','h','i','\0','f','\0'}
    printf("\n");

    char j[]="abc";
    char k[]="aBc";
    char l[]="acb";
    char m[]={'a','\0'};
    printf("%d,%d,%d",strcmp(j,k),strcmp(k,l),strcmp(l,m));//遇到第一个不相同的字符或\0则返回两者前后之差,结果:32,-33,99
    printf("\n");

    return 0;
}

注意:

1.在Xcode中会提示gets是不安全的,推荐使用fgets()。

2.strlen()只用于计算字符串长度,由于在C语言中字符串使用字符数组长度表示,所以它可以计算带有’\0’结尾的字符数组长度,但是它并不能计算其他类型的数组长度。

iOS开发系列--C语言之指针

概览

指针是C语言的精髓,但是很多初学者往往对于指针的概念并不深刻,以至于学完之后随着时间的推移越来越模糊,感觉指针难以掌握,本文通过简单的例子试图将指针解释清楚,今天的重点有几个方面:

  1. 什么是指针
  2. 数组和指针
  3. 函数指针

什么是指针

存放变量地址的变量我们称之为“指针变量”,简单的说变量p中存储的是变量a的地址,那么p就可以称为是指针变量,或者说p指向a。当我们访问a变量的时候其实是程序先根据a取得a对应的地址,再到这个地址对应的存储空间中拿到a的值,这种方式我们称之为“直接引用”;而当我们通过p取得a的时候首先要先根据p转换成p对应的存储地址,再根据这个地址到其对应的存储空间中拿到存储内容,它的内容其实就是a的地址,然后根据这个地址到对应的存储空间中取得对应的内容,这个内容就是a的值,这种通过p找到a对应地址再取值的方式成为“间接引用”。这里以表格形式列出a和p的存储以帮助大家理解上面说的内容:

接下来,看一下指针的赋值

//
//  main.c
//  Point
//
//  Created by Kenshin Cui on 14-7-05.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int main(int argc, const char * argv[]) {

    int a=1;
    int *p;
    p=&a; //也可以直接给指针变量赋值:int *p=&a;
    printf("address(a)=%x,address(p)=%x\n",&a,p); //结果:address(a)=5fbff81c,address(p)=5fbff81c
    printf("a=%d,p=%d\n",a,*p); //结果:a=1,p=1
    *p=2;
    printf("a=%d,*p=%d\n",a,*p); //结果:a=2,p=2

    int b=8;
    char c= 1;
    int *q=&c;
    printf("address(b)=%x,address(c)=%x\n",&b,&c);//结果:
    printf("c=%d,q=%d\n", c, *q); //结果:c=1,q=2049,为什么q的值不是1呢?

    return 0;
}

需要说明两点:

a.int *p;中的*只是表示p变量是一个指针变量;而打印*p的时候,*p中的*是操作符,表示p指针指向的变量的存储空间(当前存储就是1),同时我们也看到了*p==a;修改了*p也就是修改了p指向的存储空间的内容,也就修改了a,所以第二次打印a=2;

b.指针所指向的类型必须和定义指针时声明的类型相同;上面指针q定义成了int型而指向了char型,结果输出*q打印出了2049,具体原因见下图(假设在16位编译器下,指针长度为2字节)

由于局部变量是存储在栈里面的,所以先存储b再存储a、p,当打印*p的时候,其实就是以p指向的地址对应的空间开始取两个字节的数据(因为定义p的时候它指向的是int型,在16位编译器下int类型的长度为2),刚好定义的b和c空间连续,所以就取到b的其中一个字节,最后*p二进制存储为“0000100000000001”(见上图黄色背景内容),十进制表示就是2049;

c.指针变量占用的空间和它所指向的变量类型无关,只跟编译器位数有关(准确的说只跟寻址方式有关);

数组和指针

由于数组的存储是连续的,数组名就是数组的地址,这样一来数组和指针就有着很微妙的关系,先看以下例子:

//
//  main.c
//  Point
//
//  Created by Kenshin Cui on 14-7-05.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

void changeValue(int a[]){
    a[0]=2;
}
void changeValue2(int *p){
    p[0]=3;
}

int main(int argc, const char * argv[]) {
    int a[]={1,2,3};
    int *p=&a[0]; //等价于:*p=a;

    printf("len=%lu\n",sizeof(int));//取得int长度为2

    //指针加1代表地址向后挪动所指向类型的长度位(这里类型是int,长度为2)
    //也就是说p指向a[0],p+1指向a[1],以此类推,所以我们通过指针也可以取出数组元素
    for(int i=0;i<3;++i){
        //printf("a[%d]=%d\n",i,a[i]);
        printf("a[%d]=%d\n",i,*(p+i));//由于a就代表数组的地址,其实这里还可以写成*(a+i),但是注意这里*(p+i)可以写成*(p++),但是*(a+i)不能写成*(a++),因为数组名是常量
    }
    /*输出结果:
     a[0]=1
     a[1]=2
     a[2]=3
     */

    changeValue(p); //等价于:changeValue(a)
    for(int i=0;i<3;++i){
        printf("a[%d]=%d\n",i,a[i]);
    }
    /*输出结果:
     a[0]=2
     a[1]=2
     a[2]=3
     */

    changeValue2(a); //等价于:changeValue2(p)
    for(int i=0;i<3;++i){
        printf("a[%d]=%d\n",i,a[i]);
    }
    /*输出结果:
     a[0]=3
     a[1]=2
     a[2]=3
     */

    return 0;
}

从上面的例子我们可以得出如下结论:

  1. 数组名a==&a[0]==*p;
  2. 如果p指向一个数组,那么p+1指向数组的下一个元素,同时注意p+1移动的长度并不固定,具体需要根据p指向的数据类型而定;
  3. 指针可以写成p++形式,但是数组名不可以,因为数组名是常量
  4. 不管函数的形参为数组还是指针,实参都可以使用数组名或指针;

扩展--字符串和指针

由于在C语言中字符串就是字符数组,下面不妨看一下字符串和数组的关系:

//
//  main.c
//  Point
//
//  Created by Kenshin on 14-7-05.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int main(int argc, const char * argv[]) {
    char a[]="Kenshin";
    printf("%x,%s\n",a,a);//结果:5fbff820,Kenshin,同一个变量a是输出字符串还是输出地址,根据格式参数而定
    printf(a); //结果:Kenshin
    printf("\n");

    char b[]="Kenshin";
    char *p=b;
    printf("b=%s,p=%s\n",b,p);//结果:b=Kenshin,p=Kenshin

    //指针存储的是地址,而数组名存储的也是地址,既然字符数组可以表示字符串,那么指向字符的指针同样也可以,如下方式可以更简单的定义一个字符串
    char *c="Kenshin"; //等价于char c[]="Kenshin";
    printf("c=%s\n",c); //结果:c=Kenshin

    return 0;
}

以上代码中注释基本已经很清楚了,这里需要指出是为什么printf(a)能够直接输出字符串呢?

我们看一下printf()的定义:int     printf(const char * __restrict, ...) __printflike(1, 2);

其实printf的参数要求是指向字符类型的指针,而结合上面的例子和我们之前说的,如果函数形参是指针类型那么可以传入函数名,因此也就能正确输出字符串的内容了。类似的还有上一篇文章中说的strcat()、strcpy()等函数均是如此。

函数指针

在弄清函数指针的问题之前,我们不妨先来看一下返回指针类型数据的函数,毕竟指针类型也是C语言的数据类型,下面以一个字符串转换为大写字符的程序为例,在这个例子中不仅可以看到返回值为指针类型的函数同时还可以看到前面说到的指针移动操作:

//
//  main.c
//  Point
//
//  Created by Kenshin Cui on 14-06-28.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

char * toUpper(char *a){
    char *b=a; //保留最初地址,因为后面的循环会改变字符串最初地址
    int len='a'-'A'; //大小写ASCII码差值相等
    while (*a!='\0') { //字符是否结束
        if(*a>'a'&&*a<'z'){//如果是小写字符
            *(a++) -= len; //*a表示数组对应的字符(-32变为小写),a++代表移动到下一个字符
        }
    }
       return b;
}

int main(int argc, const char * argv[]) {
    char a[]="hello";
    char *p=toUpper(a);
    printf("%s\n",p); //结果:HELLO
    return 0;
}

大家都是知道函数只能有一个返回值,如果需要返回多个值,怎么办,其实很简单,只要将指针作为函数参数传递就可以了,在下面的例子中我们再次看到指针作为参数进行传递。

//
//  main.c
//  Point
//
//  Created by Kenshin Cui on 14-6-28.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int operate(int a,int b,int *c){
    *c=a-b;
    return a+b;
}

int main(int argc, const char * argv[]) {
    int a=1,b=2,c,d;
    d=operate(a, b, &c);
    printf("a+b=%d,a-b=%d\n",d,c);//结果:a+b=3,a-b=-1
    return 0;
}

函数也是在内存中存储的,当然函数也有一个起始地址(事实上函数名就是函数的起始地址),这里同样需要弄清函数指针的关系。函数指针定义的形式:返回值类型 (*指针变量名)(形参1,形参2),拿到函数指针其实我们就相当于拿到了这个函数,函数的操作都可以通过指针来完成,而且通过前面的例子可以看到指针作为C语言的数据类型,可以作为参数、作为返回值,那么当然函数指针同样可以作为函数的参数和返回值:

//
//  main.c
//  Point
//
//  Created by Kenshin Cui on 14-6-28.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int sum(int a,int b){
    return a+b;
}

int sub(int a,int b){
    return a-b;
}

//函数指针作为参数进行传递
int operate(int a,int b,int (*p)(int,int)){
    return p(a,b);
}

int main(int argc, const char * argv[]) {
    int a=1,b=2;
    int (*p)(int ,int)=sum;//函数名就是函数首地址,等价于:int (*p)(int,int);p=sum;
    int c=p(a,b);
    printf("a+b=%d\n",c); //结果:a+b=3

    //函数作为参数传递
    printf("%d\n",operate(a, b, sum)); //结果:3
    printf("%d\n",operate(a, b, sub)); //结果:-1

    return 0;
}

函数指针可以作为函数参数进行传递,实在太强大了,是不是想起了C#中的委托?记得C#书籍中经常提到委托类似于函数指针,其实说的就是上面的情况。需要注意的是,普通的指针可以写成p++进行移动,而函数指针写成p++并没有意义。

iOS开发系列--C语言之预处理

概述

大家都知道一个C程序的运行包括编译和链接两个阶段,其实在编译之前预处理器首先要进行预处理操作,将处理完产生的一个新的源文件进行编译。由于预处理指令是在编译之前就进行了,因此很多时候它要比在程序运行时进行操作效率高。在C语言中包括三类预处理指令,今天将一一介绍:

  1. 宏定义
  2. 条件编译
  3. 文件包含

宏定义

对于程序中经常用到的一些常量或者简短的函数我们通常使用宏定义来处理,这样做的好处是对于程序中所有的配置我们可以统一在宏定义中进行管理,而且由于宏定义是在程序编译之前进行替换相比定义成全局变量或函数效率更高。

//
//  main.c
//  Pretreatment
//
//  Created by Kenshin Cui on 14-6-28.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>
#define PI 3.14 //宏定义一般大写
#define R 10
#define S 2*PI*R //在另一个宏里面引用了上面的宏

int main(int argc, const char * argv[]) {
    float r=10.5;
    double area=PI*r*r;
    printf("area=%.2f\n",area);

    double a=S;
    printf("a=%.2f\n",a);
    printf("PI=3.14\n");//注意输出结果不是3.14=3.14而是PI=3.14,字符串中的PI并不会被替换
#undef PI //强制终止宏定义,否则它的范围一直到文件结束
    int PI=3.1415926;
    double area2=PI*r*r;
    printf("area2=%.2f\n",area2);

    return 0;
}

宏定义实际的操作就是在预处理时进行对应替换,这个阶段不管语法是否正确,而且对于字符串中出现的宏名不会进行替换。宏定义的功能事实上是非常强大的,除了简单的常量替换还可以传入参数:

//
//  1.2.c
//  Pretreatment
//
//  Created by Kenshin Cui on 14-7-17.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>
#define SUM(a,b) a+b
#define SUB(a,b) (a-b)
#define MUL (a,b) (a*b) //这么定义是错误的,预处理器会认为宏名为”MUL“,替换内容为”(a,b) (a*b)“

int main(int argc, const char * argv[]) {

    int a=2,b=3,c,d;
    c=SUM(a, b);
    printf("c=%d\n",c); //结果:c=5
    d=SUM(a, b)*2;
    printf("d=%d\n"); //结果:8,为什么不是10呢?因为替换后:d=a+b*2也就是2+3*2=8

    int e=SUB(b, a)*2;
    printf("(b-a)*2=%d\n",e); //结果:2,如果SUB定义时不加括号这里应该是-1

    return 0;
}

上面我们可以看出带参数的宏功能很强大,有点类似于函数,同函数不同的是它只是简单的替换,不涉及存储空间分配,参数、返回值等问题,但是由于它在预处理阶段展开,所以一般效率较高。使用带参数的宏需要注意的就是结果最好用括号括起来否则很容易出现问题(在上面的SUM例子中我们应该已经看到了);还有一点就是带参数的宏定义时名称和参数之间不要有空格。

条件编译

条件编译其实就是在编译之前预处理器根据预处理指令判断对应的条件,如果条件满足就将对应的代码编译进去,否则代码就根本不进入编译环节(相当于根本就没有这段代码)。

//
//  main.c
//  Pretreatment
//
//  Created by Kenshin Cui on 14-06-28.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>
#define COUNT 1

int main(int argc, const char * argv[]) {

//判断是否定义了 COUNT 宏
#if defined(COUNT) //等价于:#ifdef COUNT,相反如果判断没有定义过则可以通过#if !defined(COUNT)或者#ifndef COUNT
    printf("COUNT defined\n");
#endif

//判断宏定义COUNT是否等于1
#if COUNT==1
    showMessage("hello,world!\n");
#else
    say();
#endif

    return 0;
}

文件包含

文件包含指令#include在前面也多次使用过,这里再次强调一下。首先使用#include“xxx”包含和使用#include <xxx>包含的不同之处就是使用<>包含时,预处理器会搜索C函数库头文件路径下的文件,而使用“”包含时首先搜索程序所在目录,其次搜索系统Path定义目录,如果还是找不到才会搜索C函数库头文件所在目录。

另外在使用#include的时候我们需要注意包含文件的时候是不能递归包含的,例如a.h文件包含b.h,而b.h就不能再包含a.h了;还有就是重复包含虽然是允许的(这里指的是重复包含头文件)但是这会降低编译性能,不妨看一下下面的例子:

上面有三段代码,在main.c和person.h中都包含了message.h而main.c自身又包含了person.h,这样程序在预处理阶段会对包含内容进行替换,替换后mian.c中包含了两个#include “message.h”虽然没有报错,但这会影响编译的性能,正确的做法应该是这样的:

其实就是用宏定义判断一个宏是否定义了,如果没有定义则会定义这个宏,这样以来如果已经包含过则这个宏定义肯定已经定义过了,即使再包含也不会重新定义了,下面的代码也就不会包含进去。

 

iOS开发系列--C语言之存储方式和作用域

概述

基本上每种语言都要讨论这个话题,C语言也不例外,因为只有你完全了解每个变量或函数存储方式、作用范围和销毁时间才可能正确的使用这门语言。今天将着重介绍C语言中变量作用范围、存储方式、生命周期、作用域和可访问性。

  1. 变量作用范围
  2. 存储方式
  3. 可访问性

变量作用范围

在C语言中变量从作用范围包括全局变量和局部变量。全局变量在定义之后所有的函数中均可以使用,只要前面的代码修改了,那么后面的代码中再使用就是修改后的值;局部变量的作用范围一般在一个函数内部(通常在一对大括号{}内),外面的程序无法访问它,它却可以访问外面的变量。

//
//  main.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int a=1;
void changeValue(){
    a=2;
    printf("a=%d\n",a);
}
int main(int argc, const char * argv[]) {
    int b=1;
    changeValue(); //结果:a=2
    printf("a=%d,b=%d\n",a,b); //结果:a=2,b=1 ,因为changeValue修改了这个全局变量
    return 0;
}

变量存储方式

C语言的强大之处在于它能直接操作内存(指针),但是要完全熟悉它的操作方式我们必须要弄清它的存储方式。存储变量的位置分为:普通内存(静态存储区)、运行时堆栈(动态存储区)、硬件寄存器(动态存储区),当然这几种存储的效率是从低到高的。而根据存储位置的不同在C语言中又可以将变量依次分为:静态变量、自动变量、寄存器变量。

静态变量

首先说一下存储在普通内存中的静态变量,全局变量和使用static声明的局部变量都是静态变量,在系统运行过程中只初始化一次(在下面的例子中虽然变量b是局部变量,在外部无法访问,但是他的生命周期一直延续到程序结束,而变量c则在第一次执行完就释放,第二次执行时重新创建)。

//
//  2.1.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int a=1; //全局变量存储在静态内存中,只初始化一次

void showMessage(){
    static int b=1; //静态变量存储在静态内存中,第二次调用不会再进行初始化
    int c=1;
    ++b;
    a+=2;
    printf("a=%d,b=%d,c=%d\n",a,b,c);
}

int main(int argc, const char * argv[]) {
    showMessage(); //结果:a=3,b=2,c=1
    showMessage(); //结果:a=5,b=3,c=1
    return 0;
}

自动变量

被关键字auto修饰的局部变量是自动变量,但是auto关键字可以省略,因此可以得出结论:所有没有被static修饰的局部变量都是自动变量。

//
//  1.3.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>
#include <stdlib.h>

int main(int argc, const char * argv[]) {
    int a=1;
    auto int b=2;
    printf("a=%d,b=%d\n",a,b); //结果:a=1,b=2 ,a和b都是自动变量,auto可以省略

    //需要注意的是,上面的自动变量是存储在栈中,其实还可以存储到堆中
    char c[]="hello,world!";
    long len=strlen(c)*sizeof(char)+1;//之所以加1是因为字符串后面默认有一个\0空操作符不计算在长度内
    char *p=NULL;//可以直接写成:char *p;
    p=(char *)malloc(len);//分配指定的字节存放c中字符串,注意由于malloc默认返回“void *”需要转化
    memset(p,0,len);//清空指向内存中的存储内容,因为分配的内存是随机的,如果不清空可能会因为垃圾数据产生不必要的麻烦
    strcpy(p,c);
    printf("p=%s\n",p);//结果:p=hello,world!
    free(p);//释放分配的空间
    p=NULL;//注意让p指向空,否则p将会是一个存储一个无用地址的野指针

    return 0;
}

当然存储自动变量的栈和堆其实是两个完全不同的空间(虽然都在运行时有效的空间内):栈一般是程序自动分配,其存储结果类似于数据结构中的栈,先进后出,程序结束时由编译器自动释放;而堆则是开发人员手动编码分配,如果不进行手动释放就只有等到程序运行完操作系统回收,其存储结构类似于链表。在上面的代码中p变量同样是一个自动变量,同样可以使用auto修饰,只是它所指向的内容放在堆上(p本身存放在栈上)。

这里说明几点:malloc分配的空间在逻辑上连续,物理上未必连续;p必须手动释放,否则直到程序运行结束它占用的内存将一直被占用;释放p的过程只是把p指向的空间释放掉,p中存放的地址并未释放,需要手动设置为NULL,否则这将是一个无用的野指针;

寄存器变量

默认情况下无论是自动变量还是静态变量它们都在内存中,不同之处就是自动变量放在一块运行时分配的特殊内存中。但是寄存器变量却是在硬件寄存器中,从物理上来说它和内存处在两个完全不同的硬件中。大家都是知道寄存器存储空间很小,但是它的效率很高,那么合理使用寄存器变量就相当重要了。什么是寄存器变量呢?使用register修饰的int或char类型的非静态局部变量是寄存器变量。没错,需要三个条件支撑:register修饰、必须是int或char类型、必须是非静态局部变量。

除了存储位置不同外,寄存器变量完全符合自动变量的条件,因此它的生命周期其实是和自动变量完全一样的,当函数运行结束后它就会被自动释放。由于寄存器空间珍贵,因此我们需要合理使用寄存器变量,只有访问度很高的变量我们才考虑使用寄存器变量,如果过多的定义寄存器变量,当寄存器空间不够用时会自动转化为自动变量。

//
//  1.3.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

int main(int argc, const char * argv[]) {
    register int a=1;
    printf("a=%d\n",a);
    return 0;
}

上面我们说到变量的存储类型,其实在C语言中还有两种存储类型:常量存储区和代码存储区,分别用于存储字符串常量、使用const修饰的全局变量以及二进制函数代码。

可访问性

在C语言中没有其他高级语言public、private等修饰符,来限定变量和函数的有效范围,但是却有两个类似的关键字能达到类似的效果:extern和static。

extern

extern作用于变量

我们知道在C语言中变量的定义顺序是有严格要求的,要使用变量则必须在使用之前定义,extern用于声明一个已经存在的变量,这样一来即使在后面定义一个变量只要前面声明了,也同样可以使用。具体的细节通过下面的代码相信大家都可以看明白:

//
//  2.1.c
//  ScopeAndLifeCycle
//
//  Created by Kenshin Cui on 14-7-12.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

//如果在main函数下面定义了一个变量a,如果在main上面不进行声明是无法在main中使用a的;
//同样如果只进行了extern声明不进行定义一样会报错,因为extern并不负责定义变量a而仅仅是声明一个已经定义过的变量;
//当然如果说在main上面定义int a;去掉main下面的定义同样是可以的,相当于在上面定义,但如果两个地方都定义a的话(main上面的extern去掉),则程序认为上面的定义是声明,只是省略了extern关键字;

//第一种情况,在下面定义,不进行声明,报错
int main(int argc, const char * argv[]) {

    printf("a=%d\n",a);
    return 0;
}

int a;

//第二种情况,在上面定义,正确
int a;
int main(int argc, const char * argv[]) {

    printf("a=%d\n",a);
    return 0;
}

//第三种情况,在下面定义在上面声明,正确
extern int a;
int main(int argc, const char * argv[]) {

    printf("a=%d\n",a);
    return 0;
}

int a;

//第四种情况,只在上面声明(编译时没有问题,因为上面的声明骗过了编译器,但运行时报错,因为extern只能声明一个已经定义的变量),错误
extern int a;
int main(int argc, const char * argv[]) {

    printf("a=%d\n",a);
    return 0;
}

//第五种情况,上下同时定义(这种方式是正确的,因为上面的定义会被认为是省略了extern的声明),正确
int a;
int main(int argc, const char * argv[]) {

    printf("a=%d\n",a);
    return 0;
}
int a;
//其实下面的情况也是不会出错的
int a;
int a;
int main(int argc, const char * argv[]) {

    printf("a=%d\n",a);
    return 0;
}
int a;
int a;

//第六种情况,将全局变量声明为局部变量,但是它的实质还是全局变量,正确
int a;
int main(int argc, const char * argv[]) {
    extern int a;
    printf("a=%d\n",a);
    return 0;
}
int a;

//第七种情况,在函数内部重新定义一个变量a,虽然不会报错,但是两个a不是同一个
int a;
int main(int argc, const char * argv[]) {
    int a;
    printf("a=%d\n",a);//注意这里输出的a其实是内部定义的a,和函数外定义的a没有关系
    return 0;
}
int a;

如果两个文件同时定义一个全局变量,那实质上他们指的是同一个变量。从下面的例子可以看出,在main.c中修改了变量a之后message.c中的变量a值也修改了。

需要注意,在上面的代码中无论在message.h中将a定义前加上extern,还是在main.h中的a定以前加上extern结果都是一样的,extern同样适用。和在单文件中一样,不能两个定义都添加extern,否则就没有定义了。如果把message.c中a的定义(或声明)去掉呢,那么它能否访问main.c中的全局变量a呢,答案是否定的(这和在一个文件中定义了一个函数在另一个文件不声明就直接用是类似的)。

extern作用于函数

extern作用于函数就不再是简单的声明函数了,而是将这个函数作为外部函数(当然还有内部函数,下面会说到),在其他文件中也可以访问。但是大家应该已经注意到,在上面的代码中message.c中的showMessage前面并没有添加extern关键字,在main.c中不是照样访问吗?那是因为这个关键字是可以省略的,默认情况下所有的函数都是外部函数。

和作用于变量不同,上面main.c和message.c中的extern都可以省略,在这里extern的作用就是定义或声明一个外部函数。从上面可以看到在不同的文件中可以定义同一个变量,它们被视为同一个变量,但是需要指出的是外部函数在一个程序中是不能重名的,否则会报错。

static

static作用于变量

其实在前面的例子中我们已经看到static关键字在变量中的使用了,在例子中使用static定了一个局部变量,而且我们强调static局部变量在函数中只被初始化一次。那么如果static作用于全局变量是什么效果呢?如果static作用于全局变量它的作用就是定义一个只能在当前文件访问的全局变量,相等于私有全局变量

从上面的输出结果可以看出message.c中的变量a和main.c中的变量a并不是同一个变量,事实上message.c中的变量a只能在message.c中使用,虽然main.c中的变量a是全局变量但是就近原则,message.c会使用自己内部的变量a。当然,上面例子中main.c中的变量a定义成静态全局变量结果也是一样的,只是这样如果还有其他源文件就不能使用a变量了。但是main.c中的a不能声明成extern,因为main.c不能访问message.c中的变量a,这样在main.c中就没变量a的定义了。

static作用于函数

static作用于函数和作用于变量其实是类似的,如果static作用于函数则这个函数就是内部函数,其他文件中的代码不可以访问。下面的代码在运行时会报错,因为mesage.c中的showMessage()函数是私有的,在main.c中尽管进行了声明,可以在编译阶段通过,但是在链接阶段会报错。

总结

最后做一下简单总结一下:

  1. extern作用于变量时用于声明一个已经定义的变量,但是并不能定义变量;使用extern你可以在其他文件中使用全局变量(当然此时extern可以省略);
  2. extern作用于函数时与它作用于全局变量有点类似,声明这个函数是外部函数,其他文件可以访问,但不同的是当它作用于函数时不仅可以声明函数还可以定义函数(用在函数定义前面),不管是定义还是声明都可以省略,C语言默认认为函数定义或声明都是外部函数;
  3. static作用于变量时,该变量只会定义一次,以后在使用时不会重新定义,当static作用于全局变量时说明该变量只能在当前文件可以访问,其他文件中不能访问;
  4. static作用于函数时与作用于全局变量类似,表示声明或定义该函数是内部函数(又叫静态函数),在该函数所在文件外的其他文件中无法访问此函数;

iOS开发系列--C语言之构造类型

概述

在第一节中我们就提到C语言的构造类型,分为:数组、结构体、枚举、共用体,当然前面数组的内容已经说了很多了,这一节将会重点说一下其他三种类型。

  1. 结构体
  2. 枚举
  3. 共用体

结构体

数组中存储的是一系列相同的数据类型,那么如果想让一个变量存储不同的数据类型就要使用结构体,结构体定义类似于C++、C#、Java等高级语言中类的定义,但事实上它们又有着很大的区别。结构体是一种类型,并非一个变量,只是这种类型可以由其他C语言基本类型共同组成。

//
//  main.c
//  ConstructedType
//
//  Created by Kenshin Cui on 14-7-18.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

//结构体类型Date
struct Date{
    int year;
    int month;
    int day;
};

struct Person{
    char *name;
    int age;
    struct Date birthday;//一个结构体中使用了另一个结构体类型,结构体类型变量声明前必须加上struct关键字
    float height;
};

int main(int argc, const char * argv[]) {
    struct Person p={"Kenshin",28,{1986,8,8},1.72};
    //定义结构体变量并初始化,不允许先定义再直接初始化,例如:struct Person p;p={"Kenshin",28,{1986,8,8},1.72};是错误的,但是可以分别赋值,例如p.name="Kenshin"

    printf("name=%s,age=%d,birthday=%d-%d-%d,height=%.2f\n",p.name,p.age,p.birthday.year,p.birthday.month,p.birthday.day,p.height);
    //结果:name=Kenshin,age=28,birthday=1986-8-8,height=1.72,结构体的引用是通过"结构体变量.成员名称"(注意和结构体指针访问结构体成员变量区分,结构体指针使用p->a的形式访问)

    printf("len(Date)=%lu,len(Person)=%lu\n",sizeof(struct Date),sizeof(struct Person));
    //结果:len(Date)=12,len(Person)=32

    return 0;
}

对于上面的例子需要做出如下说明:

  1. 可以在定义结构体类型的同时声明结构体变量;
  2. 如果定义结构体类型的同时声明结构体变量,此时结构体名称可以省略;
  3. 定义结构体类型并不会分配内存,在定义结构体变量的时候才进行内存分配(同基本类型时类似的);
  4. 结构体类型的所占用内存大型等于所有成员占用内存大小之和(如果不考虑内存对齐的前提下);

对第4点需要进行说明,例如上面代码是在64位编译器下运行的结果(int长度4,char长度1,float类型4),Date=4+4+4=12。但是对于Person却没有那么简单了,因为按照正常方式计算Person=8+4+12+4=28,但是从上面代码中给出的结果是32,为什么呢?这里不得不引入一个概念“内存对齐”,关于内存对齐的概念在这里不做详细说明,大家需要了解的是:在Mac OS X中对齐参数默认为8(可以通过在代码中添加#pragma pack(8)改变对齐参数),如果结构体中的类型不大于8,那么结构体长度就是其成员类型之和,但是如果成员变量的长度大于这个对齐参数那么得到的结果就不一定是各个成员变量之和了。Person类型的长度之所以是32,其实主要原因是因为Date类型长度12在存储时其偏移量12不是8的倍数,考虑到内存对齐的原因需要添加4个补齐长度,这里使用表格的形式列出了具体原因:

表格具体来源请观看下面的视频(注意由于录制软件的原因前几秒不清晰但是不影响分析):

 

接下来看一下结构体数组、指向结构体的指针:

//
//  main.c
//  ConstructedType
//
//  Created by Kenshin Cui on 14-7-18.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

struct Date{
    int year;
    int month;
    int day;
};

struct Person{
    char *name;
    int age;
    struct Date birthday;
    float height;
};

void changeValue(struct Person person){
    person.height=1.80;
}

int main(int argc, const char * argv[]) {
    struct Person persons[]={
        {"Kenshin",28,{1986,8,8},1.72},
        {"Kaoru",27,{1987,8,8},1.60},
        {"Rosa",29,{1985,8,8},1.60}
    };
    for (int i=0; i<3; ++i) {
        printf("name=%s,age=%d,birthday=%d-%d-%d,height=%.2f\n",
               persons[i].name,
               persons[i].age,
               persons[i].birthday.year,
               persons[i].birthday.month,
               persons[i].birthday.day,
               persons[i].height);
    }
    /*输出结果:
     name=Kenshin,age=28,birthday=1986-8-8,height=1.72
     name=Kaoru,age=27,birthday=1987-8-8,height=1.60
     name=Rosa,age=29,birthday=1985-8-8,height=1.60
     */

    struct Person person=persons[0];
    changeValue(person);
    printf("name=%s,age=%d,birthday=%d-%d-%d,height=%.2f\n",
           persons[0].name,
           persons[0].age,
           persons[0].birthday.year,
           persons[0].birthday.month,
           persons[0].birthday.day,
           persons[0].height);
    /*输出结果:
     name=Kenshin,age=28,birthday=1986-8-8,height=1.72
     */

    struct Person *p=&person;
    printf("name=%s,age=%d,birthday=%d-%d-%d,height=%.2f\n",
           (*p).name,
           (*p).age,
           (*p).birthday.year,
           (*p).birthday.month,
           (*p).birthday.day,
           (*p).height);
    /*输出结果:
     name=Kenshin,age=28,birthday=1986-8-8,height=1.72
     */
    printf("name=%s,age=%d,birthday=%d-%d-%d,height=%.2f\n",
           p->name,
           p->age,
           p->birthday.year,
           p->birthday.month,
           p->birthday.day,
           p->height);
    /*输出结果:
     name=Kenshin,age=28,birthday=1986-8-8,height=1.72
     */

    return 0;
}

结构体作为函数参数传递的是成员的值(值传递而不是引用传递),对于结构体指针而言可以通过”->”操作符进行访问。

枚举

枚举类型是比较简单的一种数据类型,事实上在C语言中枚举类型是作为整形常量进行处理的,通常称为“枚举常量”。

//
//  main.c
//  ConstructedType
//
//  Created by Kenshin Cui on 14-7-18.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

enum Season{ //默认情况下spring=0,summer=1,autumn=2,winter=3
    spring,
    summer,
    autumn,
    winter
};

int main(int argc, const char * argv[]) {
    enum Season season=summer; //枚举赋值,等价于season=1
    printf("summer=%d\n",season); //结果:summer=1

    for(season=spring;season<=winter;++season){
        printf("element value=%d\n",season);
    }
    /*结果:
     element value=0
     element value=1
     element value=2
     element value=3
     */
    return 0;
}

需要注意的是枚举成员默认值从0开始,如果给其中一个成员赋值,其它后面的成员将依次赋值,例如上面如果summer手动指定为8,则autumn=9,winter=10,而sprint还是0。

共用体

共用体又叫联合,因为它的关键字是union(貌似数据库操作经常使用这个关键字),它的使用不像枚举和结构体那么频繁,但是作为C语言中的一种数据类型我们也有必要弄清它的用法。从前面的分析我们知道结构体的总长度等于所有成员的和(当然此时还可能遇到对齐问题),但是和结构体不同的是共用体所有成员共用一块内存,顺序从低地址开始存放,一次只能使用其中一个成员,union最终大小由共用体中最大的成员决定,对某一成员赋值可能会覆盖另一个成员。

//
//  main.c
//  ConstructedType
//
//  Created by Kenshin Cui on 14-7-20.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#include <stdio.h>

union Type{
    char a;
    short int b;
    int c;
};

int main(int argc, const char * argv[]) {
    union Type t;
    t.a='a';
    t.b=10;
    t.c=65796;

    printf("address(Type)=%x,address(t.a)=%x,address(t.b)=%x,address(t.c)=%x\n",&t,&t.a,&t.b,&t.c);
    //结果:address(Type)=5fbff7b8,address(t.a)=5fbff7b8,address(t.b)=5fbff7b8,address(t.c)=5fbff7b8

    printf("len(Type)=%d\n",sizeof(union Type));
    //结果:len(Type)=4

    printf("t.a=%d,t.b=%d,t.c=%d\n",t.a,t.b,t.c);
    //结果:t.a=4,t.b=260,t.c=65796

    return 0;
}

这里需要重点解释一个问题:为什么t.a、t.b、t.c输出结果分别是4、260、65796,当然t.c等于65796并不奇怪,但是t.a前面赋值为’a’不应该是97吗,而t.b不应该是10吗?其实如果弄清这个问题共用体的概念基本就清楚了。

根据前面提到的,共用体其实每次只能使用其中一个成员,对于上面的代码经过三次赋值最终使用的其实就是t.c,而通过上面的输出结果我们也确实看到c是有效的。共用体有一个特点就是它的成员存储在同一块内存区域,这块区域的大小需要根据它的成员中长度最大的成员长度而定。由于上面的代码是在64位编译器下编译的,具体长度:char=1,short int=2,int=4,所以得出结论,Type的长度为4,又根据上面输出的地址,可以得到下面的存储信息(注意数据的存储方式:高地址存储高位,低地址存储地位):

当读取c的时候,它的二进制是“00000000  00000001  00000001  00000100”,换算成十进制就是65796;而经过三次赋值后,此时b的存储就已经被c成员的低位数据覆盖,b的长度是二,所以从起始地址取两个字节得到的二进制数据此时是“00000001  00000100”(b原来的数据已经被c低2位数据覆盖,其实此时就是c的低2位数据),换算成十进制就是260;类似的a此时的数据就是c的低一位数据”00000100”,换算成十进制就是4。

iOS-C基础的更多相关文章

  1. IOS开发基础知识碎片-导航

    1:IOS开发基础知识--碎片1 a:NSString与NSInteger的互换 b:Objective-c中集合里面不能存放基础类型,比如int string float等,只能把它们转化成对象才可 ...

  2. iOS系列 基础篇 03 探究应用生命周期

    iOS系列 基础篇 03 探究应用生命周期 目录: 1. 非运行状态 - 应用启动场景 2. 点击Home键 - 应用退出场景 3. 挂起重新运行场景 4. 内存清除 - 应用终止场景 5. 结尾 本 ...

  3. iOS系列 基础篇 04 探究视图生命周期

    iOS系列 基础篇 04 探究视图生命周期 视图是应用的一个重要的组成部份,功能的实现与其息息相关,而视图控制器控制着视图,其重要性在整个应用中不言而喻. 以视图的四种状态为基础,我们来系统了解一下视 ...

  4. iOS系列 基础篇 05 视图鼻祖 - UIView

    iOS系列 基础篇 05 视图鼻祖 - UIView 目录: UIView“家族” 应用界面的构建层次 视图分类 最后 在Cocoa和Cocoa Touch框架中,“根”类时NSObject类.同样, ...

  5. iOS系列 基础篇 06 标签和按钮 (Label & Button)

    iOS系列 基础篇 06 标签和按钮 (Label & Button) 目录: 标签控件 按钮控件 小结 标签和按钮是两个常用的控件,下面咱们逐一学习. 1. 标签控件 使用Single Vi ...

  6. iOS系列 基础篇 07 Action动作和输出口

    iOS系列 基础篇 07 Action动作和输出口 目录:  1. 前言及案例说明 2. 什么是动作? 3. 什么是输出口? 4. 实战 5. 结尾 1. 前言及案例说明 上篇内容我们学习了标签和按钮 ...

  7. iOS系列 基础篇 08 文本与键盘

    iOS系列 基础篇 08 文本与键盘 目录: 1. 扯扯犊子 2. TextField 3. TextView 4. 键盘的打开和关闭 5. 打开/关闭键盘的通知 6. 键盘的种类 7. 最后再扯两句 ...

  8. iOS系列 基础篇 09 开关、滑块和分段控件

    iOS系列 基础篇 09 开关.滑块和分段控件 目录: 案例说明 开关控件Switch 滑块控件Slider 分段控件Segmented Control 1. 案例说明 开关控件(Switch).滑块 ...

  9. iOS网络基础知识

    iOS网络基础知识 1.一次HTTP请求的完整过程 (1)浏览器或应用发起Http请求,请求包含Http请求Http(请求),地址(url),协议(Http1.1)请求为头部 (2)web服务器接收到 ...

  10. iOS 面试基础题目

    转载: iOS 面试基础题目 题目来自博客:面试百度的记录,有些问题我能回答一下,不能回答的或有更好的回答我放个相关链接供参考. 1面 Objective C runtime library:Obje ...

随机推荐

  1. FZU 1914 单调队列

    题目链接:http://acm.fzu.edu.cn/problem.php?pid=1914 题意: 给出一个数列,如果它的前i(1<=i<=n)项和都是正的,那么这个数列是正的,问这个 ...

  2. PHP isset() empty() isnull() 的区别

    <? isset - 检测变量是否设置 注意: isset 检测变量是否设置,并且不是 NULL. 若使用 isset() 测试一个被设置成 NULL 的变量,将返回 FALSE: empty ...

  3. strust2中使用session

    在Struts2里,如果需要在Action中使用session,可以通过下面两种方式得到1.通过ActionContext class中的方法getSession得到2.Action实现org.apa ...

  4. webdav不识别软链接?解决办法?

    webdav不识别软链接?解决办法? 在使用webdav实现公网存储共享的时候,发现webdav并不支持软链接的共享,如下源代码可以100%确定这个问题 /* ### for now, only pr ...

  5. 【转】SHELL中的IFS详解

    转自:http://smilejay.com/2011/12/bash_ifs/ 在bash中IFS是内部的域分隔符,manual中对其的叙述如下: IFS The Internal Field Se ...

  6. phpstorm常用快捷键

    mac电脑phpstorm快捷键    command + a 全选    command + c 复制    command + v 粘贴    command + z 撤消    fn+comma ...

  7. tomcat发布脚本

    #!/bin/bash #发布相关目录Tomcat_log='/home/CodePub/tomcatlog'dev_package='/home/CodePub/package'old_packag ...

  8. 命名困惑系列之一:关于state和status的粗浅研究

    牛津高阶词汇的解释 state: CONDITION OF SB/STH  状态:the mental,emotional or physical condition that a person or ...

  9. 【系统篇】从int 3探索Windows应用程序调试原理

    探索调试器下断点的原理 在Windows上做开发的程序猿们都知道,x86架构处理器有一条特殊的指令——int 3,也就是机器码0xCC,用于调试所用,当程序执行到int 3的时候会中断到调试器,如果程 ...

  10. RN 项目导入WebStorm 组件没有依赖

    你需要在项目根目录   $ npm instal 恭喜你完成 但是依然报错 npm config set registry="registry.npmjs.org"  重新配置了一 ...