[Objective-C]C语言特性(函数,变量,编译指令,指针,块)

!!这部分讲解的是OC对于C语言特性的继承,包括小范围的扩展,因此关于每个点只强调一些重要的内容以及OC的扩展内容


1. 函数定义:

    1) 如果没有写返回值类型,则默认为int类型;

    2) 随着IT行业业务的扩大,不同公司开发的代码库中函数名重名的可能性大大增强,因此会在相互使用对方的代码库时遇到重名的错误,因此OC建议各公司开发的代码库中函数名最好加上公司名称的前缀,例如NSString的NS就是NeXT STEP的缩写;

    3) 一种古老的定义函数的方法:这种方法很古老,一些程序员仍然使用,而且C和OC都支持,要求知道,在看到这种定义函数的风格时不能完全不认识

         i. 参数列表中只有参数名而没有类型,然后另起一行逐行说明每个形参的类型;

         ii. 一般形式:type FuncName(arg1, arg2...

type FuncName(arg1, arg2...)
type arg1;
type arg2;
...
{ ... }
         iii. 例如:

int add(a, b)
int a;
int b;
{ return a + b; }
!!推荐使用普通的函数定义而不要使用这种古老的方式,普通的方式更加简明清晰;


2. 函数声明:

    1) OC和C/C++一样,都是声明和定义分开的;

    2) OC的函数声明和C一模一样,有两种版本,一种是不带参数名的,一种是包括参数名的,但显然更推荐后者,见名知意;


3. 函数参数传递:

    1) 和C语言一样,只有值传递,不管是何种数据类型(包括指针类型);

    2) 对于交换变量等操作,一定要使用指针传递;


4. 数组作为函数参数:

    1) 和C语言一样,传递的都是数组的首地址,就是一个指针;

    2) 用法和C也一样,如func(int a[])、func(int a[10])等;


5. 函数递归:完全和C一样;


6. 内部函数和外部函数:

    1) 内部函数就是定义时用static修饰的函数,此类函数只能被当前源文件使用;

    2) 外部函数就是定义时用extern修饰(什么都不写也默认是extern的),此类函数可以不仅可以被当前源文件使用,也可以被其它源文件使用;

    3) 使用外部函数也是要先声明函数原型的,这个道理很简单,不管是在同一个文件中函数先使用后定义还是在一个文件中使用另一个文件中的函数,都有一个共同点,那就是在调用这些函数的时候都“看不见”函数的定义,因此都需要先声明才行;


7. 局部变量和全局变量:

    1) 局部变量和C语言的定义一模一样:

         i. 如果不初始化则值是随机值;

         ii. 同名局部变量将在定义的局部范围内屏蔽外部或全局的标量;    

    2) 内部全局变量和外部全局变量:

         i. 外部全局变量就是什么修饰都不加的在函数外部定义的变量,此类变量可以被所有源文件使用;

         ii. 内部全局变量就是在定义变量时用static修饰的变量,此类变量只能被当前源文件使用,外部文件即使有同名变量也会屏蔽它,并且可以定义在局部,但其效果是全局的,也存储在程序的静态区(一次初始化后就不会在初始化了);

         iii. 在一个文件中使用其它文件中定义的外部变量:

              a. 必须声明!!对!使用外部变量是需要声明的,声明语句就是:extern type var_name;

              b. 注意!用extern修饰的上述语句不是定义了一个外部变量,而是声明要使用一个其它文件中定义的外部变量!这是声明语句!!

              c. 外部变量的定义就是普通的type var_name,什么修饰都不用加的!!

!!还有一种使用全局变量的情况会被当成是在使用其它文件中定义的外部全局变量,那就是在同一个文件中一个全局变量定义在后但是使用在前,这种情况下编译器也会认为该变量是一个在其它文件中定义的外部全局变量,因此也需要在使用前用extern声明一下才行;

         iv. 全局变量总结:使用示例

// 1.c
int g_var1 = 1;
int g_var2 = 2;


// 2.c

static g_var4 = 4;

void test() {
	// 真正定义在外部文件中
	extern int g_var1;
	extern int g_var2;
	// 定义在同一个文件中的后面也算外部全局变量
	extern int g_var3;

	printf("%d\n", g_var1 + g_var2 + g_var3 + g_var4);
}

int g_var3 = 3;

    3) 涉及多个文件的编译需要将所有的文件都放在一起编译;

    4) 所有类型的全局变量如果都不初始化则编译器会给出一个默认初始值,那就是在内存中各个位都清0,即指针会是NULL,整型会是0,浮点型会是0.0等;


8. 预处理编译指令:

    1) 完全保留的C的预编译指令,同时也作出了一定的扩展;

    2) 预处理指令的作用仅仅就是在源文件中做查找和替换(对象是原文件中的字符串),其指令并不参与程序的运行;

    3) #define和#undef:定义和取消宏,仅仅就是用宏来代替某个字符串,有不带参宏和带参宏;

!!使用的时候还是要注意,尽量要多用括号,以避免不必要的错误!!

!#undef的使用很简单,只需在后面加上一个宏名,就可以提前取消该宏在接下来的作用;

    4) #ifdef/#ifndef - #else - #endif组合:

        i. 最主要的作用是跨平台和移植;

        ii. 如果定义或没定义某个宏就编译下面的代码,否则就编译#else后面的代码;

        iii. 在调试时,可以使用clang的-D选项在命令行定义宏;

    5) 更一般的#if - #elif - #else - #endif组合,例如:

#define N  15
#if N > 15
...
#elif N > 10
...
#else
...
#endif

!!这些都只起到编译效果,不要滥用;

    6) #import对#include的扩展:

        i. OC中只有#import没有#include;

        ii. #include不会检查重复导入,比如在a中导入了b和c,而在b中也导入了c,那么a中就导入了两遍c,编译时会产生错误,#include不做重复导入的检查,因此必须劳烦地使用#ifdefine、#ifndefine等做保护;

        iii. #import更加智能,可以自动检查重复导入,让重复导入只导入一遍;

        iv. 其余方面和C语言一样,<>表示系统搜索路径,""表示用户自定义的文件,如果有路径就会从指定路径中寻找,否则就从当前路径中寻找,如果都找不到最后在从系统搜索路径中找;


9. 指针的定义和使用:

    1) 和C语言完全一样,用&取地址,用*获取存储单元本身;

    2) &和*的优先级一样,并且都是右结合的,也就是说是从右往左算的,比如*&a,就是先对a取地址再取该地址指向的存储单元本身;

    3) 定义指针变量的时候建议*和类型紧贴在一块儿,表示一种类型的指针类型,例如:int* a = &b; // int和*最好紧贴在一起,养成良好的习惯;


10. 指针和数组:

    1) 和C语言完全一样;

    2) *(p + i)和p[i]完全等价;

    3) 数组名就是指向数组首元素的常量指针,不可修改其值;

    4) 指针的实质就是指示了指针在偏移一个单位时移动的距离,该距离就是所指向的类型的大小;


11. 多维数组和指向数组的指针:

    1) 多维数组的存储方式其实也是按照一维数组的方式顺序线性存放的;

    2) 比如int arr[3][5]中,arr + 1的指针偏移距离其实有5sizeof(int)那么多,因此arr + i的类型其实是一个指向5个元素的一维数组的指针,其偏移1单位,移动了整整5个int的大小,因此其类型应该为int (*)[5]!!!

!!可以这样理解,int a[5]是一个一维数组,那么*a就是一个指向该一维数组的指针了,该指针偏移一单位,距离偏移5个int的大小,所以其类型就是int (*)[5],其中5不能丢,它只是了指针偏移一单位就要挪多少个int大小;

!!因此上例中arr + i就是指向一维数组的指针,而arr[i](也就是*(arr + i))是普通的指向一个type单元大小的指针;


11. 使用字符指针时的安全问题:

    1) 可以使用字符指针定义一个字符串常量,比如:char* p = "abcde"; 但是p指向的是一个字符串常量,指向的位置位于静态区,因此不能通过p来修改该字符串常量,比如p[1] = 'x'; 是错误的,会直接报错;

    2) 因此同样不能用一个字符指针来接受字符串输入,比如:char* p; scanf("%s", p); 因为p所指向的区域位置,所以不能接受输入,否则会导致运行错误,就是在使用字符指针呢的时候一定要知道它指向那个区域;


12. 函数指针:这里有些东西是当时学C语言时没讲到的

    1) 原来讲C预言时定义函数指针的形式是:return_type (*pFunctionName)(var_type_list);

    2) 这种方式定义了函数指针指向的函数类型,有返回值、形参列表(只包含类型就行),但是还有一种更加灵活的方式,那就是不定义形参列表(形参列表直接为空,什么都不填!),但是空不代表就是void,如果写void的话还是非空的,因为void也是一种类型;

    3) 使用空的这种形式更加灵活,可以为编程带来更多的可能性,在给指针赋值时,只要函数的返回类型和定义指针时相同,具体形参列表如何无所谓,这就导致可以将很多不同形参的函数赋给同一个指针,例如:

#include <stdio.h>

int f0(int a, int b) {
	return 0;
}

int f1(float a, float b) {
	return 1;
}

int f2(char* s) {
	return 2;
}

int main() {
	int (*pf)();

	pf = f0;
	printf("%d\n", (*pf)(1, 2));

	pf = f1;
	printf("%d\n", (*pf)(1.5, 2.0));

	pf = f2;
	printf("%d\n", (*pf)("abcdefg"));

	return 0;
}
!最后的输出当然是0,1,2了;

!!这种方式的好处就是一个指针能接受多种不同类型的函数,如果定义指针时指定了形参列表,则给指针赋值时必须是严格复合该形参列表的函数才能对其赋值;


13. 其余指针特性:函数指针作为函数参数、指针数组、指针的指针等完全和C语言一样;


14. 关于结构体和typedef:

    1) 结构体的使用和定义完全和C语言一样,但这里有几点需要强调;

    2) OC在定义结构体变量时同样必须加struct关键字,否则会报错,这和ANSI C规定一样;

    3) typedef作用同样和C语言一样,经常用来简化定义结构体变量时必须要用到的struct的关键字;

    4) typdef在OC中可以定义自身,比如:

struct Point {...}
typedef struct Point Point;
    5) 关于结构体和结构体数组的初始化:

         i. 仍然和C语言一样,只能在定义时初始化(可以直接使用赋值运算符),初始化使用花括号,如果是结构体数组,则使用花括号嵌套花括号的方式初始化;

         ii. 不能在定义完之后再用赋值运算符直接对其赋值;

         iii. 比C语言更近一步的是OC的所有初始化如果没有显式赋值都会自动给一个默认初始化值,因此不管有没有给结构体或者结构体初始化,或者是否完全地初始化,都会给没显式初始化的值赋于默认值(所有位清0);


15. OC的匿名函数——块:

    1) 块是OC的lambda函数,也是函数闭包的一种用法,它允许一段代码可以被当做值一样使用(就和float、int类型的值差不多),可以参与函数传参等,简化了函数调用,比使用函数指针作为参数更加灵活和方便;

    2) 定义块的值:

         i. 一般形式:^[return_type](type1 arg1, type2 arg2...) {...};

         ii. 所谓块的值就是块中代码的实现了,我们可以把着一整段代码打包当做一种值来处理;

         iii. 定义块必须使用符号^;

         iv. 返回值类型可以不写,因为OC可以自动从代码块中的return语句推断出返回的值是什么类型的;

         v. 如果没有参数则参数列表可以为空,但最好不要直接放空,写一个占位void也行,这样代码更清晰;

    3) 定义块变量:

         i. 变量的值就按照上面的方法给出即可;

         ii. 定义变量是必须要先有类型的,而块变量的类型也是通过^符号定义出来的;

         iii. 块类型的定义格式:return_type (^)(arglist); // 参数列表只需要类型不需要参数名

         iv. 定义块变量:return_type (^var_name)(arglist); 或者直接初始化,例如:int (^int_max)(int, int) = ^(int a, int b) { return a > b ? a : b; };

!!当然也可以有了变量了以后再赋值:int_max = ^(int a, int b) { return a > b ? a : b; };

    4) 使用块变量来执行块中的代码也很简单,就跟普通的函数调用一模一样,例如上面的int_max,调用的时候可以是printf("%d\n", int_max(5, 3));

    5) 块访问局部变量:

         i. 在块内只能访问块外定义的局部变量,但是不能修改它们;

         ii. 原因是这样的,在创建块时会将块外的局部变量复制到块内(即在执行块之前就已经将副本变量载入块代码中了,因此这些变量在块内成了代码段的数据,因此无法修改);

         iii. 如果想直接访问块外的局部变量并能修改其值,则这种变量就必须先用__block关键字修饰,表示该局部变量可以被块修改,例如:

__block int a = 10;
^(void) { a = 20; } // OK!
!!但是对于静态变量、全局变量,无论是在哪儿,包括块内都是能正常修改的;


16. 块最常见的用法:作为函数参数

    1) 一般可以先用typedef将一长串复杂的块类型用一个名称来代替:typedef (^block_type_name)(arglist); 之后就可以用block_type_name类型来定义块变量了;

    2) OC通常建议将块作为函数的最后一个参数,因为这样可以兼容swift的尾随闭包!!

    3) 使用示例:

int func(int a, int b, int (^block)(int, int)) {
    return block(a, b);
}

func(5, 3, ^(int a, int b) { return a + b; });
!!传参时可以当场直接定义其值;

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值