当前位置:   article > 正文

《C专家编程》学习笔记_通过宏定义将变量变为常量

通过宏定义将变量变为常量

Chapter 1

  1. char *cp;
  2. const char *ccp;
  3. ccp = cp;
  • ccp是有指向有const 限定符的char 的指针,cp是指向没有限定符修饰的指针;左操作数具有右操作数的所有限定符(空),再加上自身的限定符(const);但是反过来赋值cp = ccp 就会违反赋值约束条件,就会产生编译告警。
  • 而const float * 是指向具有const限定符的float类型的指针,也就是说const限定符是修饰指针所指向的类型,而不是指针本身。类似的,const char ** 也不是具有限定符的类型,它是指向有const 限定符的float类型的指针的指针,也就是说const 限定符是修饰指针的指针所指向的类型;char ** 类型指向char *,const char **类型指向const char * 两者是不相容的。
  • const 并不能把变量变成常量,在一个符号前面加上const 限定符只是表示这个符号不能被赋值,但也不能防止通过程序的内部方法来修改这个值,const 最有用之处就是用它来限定函数的形参,这样该函数将不会修改实参指针所指的数据,但其他的函数却可能会修改它,这是const最一般的用法。
  1. const int limit = 10;
  2. const int * limitp = &limit;
  3. int i = 27;
  4. limitp = &i;
  • 这段代码表示limitp是一个指向常量整形的指针,也就是说const 是修饰limitp指针指向的int类型。这个指针不能用于修改被指向的int类型,但是任何时候limitp本身的值却可以改变。这样limitp就指向了不同的地址。const int * a与 int const * a两者没有区别;int * const a 与前两者不同,表示指针a的值不能修改,即a指向地址不能修改,但是可以修改a指向地址的内容;前两者表示不能通过指针a修改a所指向的内容,但是a可以指向其他地址;---参考链接:https://www.cnblogs.com/wecode/p/6146739.html
  1. 有符号数二进制原码,正数,最高位符号位是0,其余部分是数值位;负数,最高位符号位是1,其余部分是数值位;
  2. 有符号数二进制补码,正数的补码就是二进制原码,负数的补码是二进制反码加1;
  3. 有符号数SWORD16的最大值是0x7FFF,第一位拿来当做符号位;
  4. 有符号数的二进制反码,正数的反码与二进制原码相同,负数的二进制反码是二进制原码除符号位外按位取反;
  5. 有符号数转换成无符号数,有符号正数原码就是无符号正数原码,有符号负数二进制原码的补码就是转换成的无符号数;
  6. 无符号数转换成有符号数,最高位不为1,无符号数的原码就是有符号数的原码,最高位为1,将此无符号数的原码视为有符号数的原码,除最高位即符号位负号外,最低7位作为数值位;
  7. 补码=>原码,符号位0 补码就是原码;符号位1 ,其余位取反后加1 是原码

Chapter2

  1. //一种简单的方法,使一段代码第一次执行时的行为与以后的执行时不同
  2. generate_initializer(char * string)
  3. {
  4. static char separator = ' ';
  5. printf("%c %s \n", seperator, string);
  6. separator = ',';
  7. }
  8. static function turnip() {} //static 声明表示在这个文件之外不可见
  9. static 在函数内部,表示该变量的值在各个调用间一直保持延续性;
  10. extern 用于函数定义,表示全局可见(属于冗余),用于变量表示它在其他地方定义;
  11. void 作为函数的返回类型表示不反悔任何值,在指针的申声明中表示通用指针的类型,位于参数列表中表示没有参数;
  12. sizeof操作数是类型时两边必须加上括号,但操作数如果是变量则不必加括号;

 

针对最后一行,当执行i = 1,2;
赋值最运算符优先级高于逗号,i 应该是1,但是实际上i 赋值为1,接着执行常量2的运算,计算结果将丢弃.最终i的结果是1而不是2.在if 和while 等后面需要一个布尔值的时候,& 和 |就被翻译成&& 和 || ;如果在一般的表达式里,就被解释成位操作符。
操作符的结合性,在几个操作符具有相同优先级时决定先执行哪一个,赋值运算符具有右结合性,位操作符 & 和 | 具有左结合性。


不能返回指向局部变量的指针

  1. /* 将源文件的timestamp转换为表示当地格式日期的字符串 */
  2. char * localized_time(char * filename)
  3. {
  4. struct tm *tm_ptr;
  5. struct stat stat_block;
  6. char buffer[120];
  7. /* 获得源文件的timestamp, 格式为time_t */
  8. stat(filename, &stat_block);
  9. /* 把UNIX的time_t转换为tm结构,里面保存当地时间 */
  10. tm_ptr = localtime(&stat_block.st_mttime);
  11. /* 把tm结构转换成以当地日期格式表示的字符串 */
  12. strftime(buffer, sizeof(buffer), "%a %b %e %T %Y",tm_ptr);
  13. return buffer;
  14. }

// 这段代码的问题在于最后一行返回局部变量的指针,当函数结束时,由于该变量已经被销毁,即当控制流离开
//声明自动变量范围时,局部变量自动失效,无法知道这个局部变量指针所指的内容是什么?
解决办法有
1.返回一个指向字符串常量的指针;
2.使用全局申明的数组,但是也要防止其他人修改这个全局数组;
3.使用静态数组,将buffer声明为static char buffer[120],这可以防止其他人修改这个数组只有拥有指向该数组的指针的函数才能修改这个静态数组,但是函数的下一次调用将会覆盖这个数组的内容,另外,和全局数组一样,大型缓冲区闲置不用是非常浪费内存的;
4.显式分配一些内存,

  1. char * func() {
  2. char * s = malloc(120);
  3. ……
  4. return s;
  5. }

这个方法具有静态数组的优点,每次调用会创建一个新的缓冲区,所以该函数以后的调用不用覆盖以前的返回值。适用于多线程的代码。缺点是要求程序员必须承担内存管理的责任,会增加内存尚在适用就释放或者内存泄露的风险;
5.最好的解决方法是要求调用者分配内存来保存函数的返回值。为了提高安全性,调用者应该同时指定缓存区的大小

  1. void func(char * result, int size) {
  2. ……
  3. strncpy(result, "That'd be in the data segment, Bob", size);
  4. }
  5. buffer = malloc(size);
  6. func(buffer, size);
  7. ……
  8. free(buffer);

Chapter 3

C语言晦涩的声明语句是一个缺点;
union与结构体类似,但是内存布局上存在关键性的区别,在结构体中所有成员依次存储,在union中所有成员的都从零开始存储。这样,每个成员的位置都重叠在一起:在某一时刻,只有一个成员正真存储于该地址。union与struct结构一样,只是用union取代了关键字struct。union一般用来节省空间,也可以把同一个数据解释成两种不同的东西而不需要赋值或强制类型转换

  1. union bits32_tag{
  2.         int whole; /*一个32位的值*/
  3.         struct {char c0, c1, c2, c3;} byte;/*48位的字节*/
  4. }value;

用enum完成的事都可以用#define来完成;默认情况下enum从整形值0开始,如果对某个标识符赋值,那么紧随其后的那个标识符的值就比所赋的值大1。

enum Day {MON=1, TUE, WED, THU, FRI, SAT, SUN};

C语言声明的优先级规则
const关键字的后紧跟类型说明符,那么它作用于类型说明符。其他情况下,关键字作用于左边紧邻的指针星号*。
合理使用const关键字可以使编译器自然地保护那些不希望被改变的参数,防止其被无意的代码修改。
优先级从高到低依次是:
1.声明中被括号括起来的部分;
2.后缀操作符:括号()表示这个是函数,方括号[]表示这是一个数组;
3.前缀操作符:星号* 表示 “指向...的指针”
用优先级规则来分析C语言声明,char * const *(*next)();
1.首先找标识符“next”,并注意到它被括号括住;
2.把括号里的东西作为一个整体,得出是“next是一个指向...的指针”
3.然后考虑括号外面的东西,在星号前缀和括号后缀之间作出选择,
4.后缀运算符优先级高于前缀运算符,右边是括号,得出“next是一个函数指针,指向一个返回...的函数”
5.然后处理前缀运算符,得出指针所指的内容
6.把char * const 解释指向字符的常量指针
总结就是,next是一个指针,指向一个函数,函数返回另一个指针,该指针指向一个指向char 类型的常量指针
char *( * c[10])(int **p);声明是c是一个数组,数组中每个元素是一个指针,这些指针都是具有(int **p)形参的指针函数,函数返回一个指向char类型的指针,函数的形参(int **p) p是一个指针,指向一个指向int的指针;
 

  • typedef 并不创建变量,而是宣称“这个名字是指定类型的同义词”
  • void (*signal(int sig, void(*func)(int)))(int);分析这个声明,signal前面有*结合后面的括号说明是函数且返回指针,参数是int和函数指针;返回的指针指向一个函数,函数接收一个int类型参数并返回void。
  • 用typedef定义简化这个函数,分为两步
  1. typedef void(* ptr_to_func) (int);
  2. //typedef声明表示ptr_to_func是这种函数指针结构的别名,可以用ptr_to_func在其他地方替换它代表的函数指针结构
  3. /*表示ptr_to_func是一个函数指针,函数接收int参数,返回void*/
  4. ptr_to_func signal (int, ptr_to_func);
  5. /* signal是一个函数,它接受两个参数,一个是int,一个是ptr_to_func,返回ptr_to_func*/

typedef int x[10]和#define x int[10]的区别
1.typedef定义的类型别名不能扩展

  1. #define peach int
  2. unsigned peach i; /* 没问题 */
  3. typedef int banana;
  4. unsigned banana i; /* 错误!非法 */

2.在连续的变量声明中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,用#define定义的类型无法保证。

  1. #define int_ptr int *
  2. int ptr chalk, cheese;
  3. // 经过宏扩展变成int * chalk, cheese;这使得chalk和cheese是不同的类型
  4. typedef char * char_ptr;
  5. char_ptr Bentley, Rolls_Royce;
  6. // Bentley和Rolls_Royce的类型依然相同

typedef声明引入了my_type这个“别名”作为“struct my_tag {int i;}”的简写形式,但同时引入了结构标签my_tag,

  1. typedef struct my_tag {int i;} my_type;
  2.     struct my_tag variable_1;
  3. my_type variable_2;

但是如果用同一个标志符表示标签和别名,那么以后使用这个标志符号时前面就不必使用关键字struct,但是这个向灌输了一种
完全错误的思维方式;为了使代码清晰,结构标签和typedef别名不能用相同的标志符。

  1. typedef struct fruit {int weight,price;} fruit; /* 定义了结构标签fruit和别名fruit */
  2.         struct veg{int weight, price;} veg; /* 定义了结构标签veg和变量veg */
  3. struct fruit mandarin; /* 使用结构标签fruit */
  4.        fruit mandarin; /* 使用结构类型fruit */
  5. struct veg potato; /* 可以接受 */
  6. veg potato; /* 不能接受 */

链接:函数指针与指针函数的解释有助于理解复杂的函数声明,参考---https://www.cnblogs.com/code1527/p/3249027.html 

始终从内往外找标志符;
优先级:被括号括起来的声明 > () 和 []  >  *;

int *pfun(int, int)

pfun左边*右边(),()优先级高于*,pfun(int,int)结合,pfun是函数,然后从pfun函数名往左找返回值,找到*,* pfun函数返回指针,然后继续int * pfun(), 指针指向int类型;总结下来就是,pfun是一个函数,返回指向int类型的指针

int (*pfun)(int, int);

找到标识符pfun,pfun被()和*一起括起来后优先级高,(*pfun)是一个指针,往右读结合()后,(*pfun)(int, int)说明指针指向函数,然后从(*pfun)往左找函数的返回类型int;总结是pfun是一个指针,指向返回int类型的函数.

int *(*x[10])(void);

找到标志符x,x左侧*右侧[],[]优先级高于*,x结合[]得到x是一个数组x[],然后与*括起来得到(*x[10])表示数组元素是指针,(*x[10])左侧*右侧(),()优先级高于*,则(*x[10])(void)表示数组的元素是指针,而指针指向参数为void的函数,最后从(*x[10])向左找函数的函数类型*,*(*x[10])(void)表示指针指向的函数返回类型是指针,然后继续往左int *(*x[10])(void)指针指向一个int类型;总结是x是数组,数组的元素是指针,指针指向函数,函数返回指向int类型的指针。

int (*ff(int))(int *, int);

找到标志符ff,左侧*右侧(),()优先级高于*,ff()表示ff函数,然后被括号括起来(*ff(int)),ff的左侧是*表示函数的返回类型是指针,然后结合右侧括号后得到(*ff(int))(int*,int),ff函数返回的指针指向一个参数为(int *,int)的函数,然后从(*ff(int))往左找函数的返回类型是int;总结是,ff是一个函数,函数返回一个指针,指针指向参数为(int*, int)返回int类型的函数;这样的用法晦涩难懂,一般会这样用typedef int(*PF)(int*, int); PF是一个指针,用typedef声明后,则PF成为函数指针“类型”,可以当做函数的返回类型;
PF ff(int),用PF声明函数,这样来替代int (*ff(int))(int*,int)。
 

Chapter 4

为什么指针和数组很多情况下会被错误认为是可以互换的?答案是对数组的引用总是可以写成对指针的引用;
extern char a[] 与extern char a[100]等价,都表示a是一个数组,也就是一个内存地址。
编译器并不需要知道数组总共有多长,因为它只产生偏离起始地址的偏移地址;而指针的访问必须明确知道指针的地址。
数组和指针的访问不同在于,编译器符号表存储的是数组的首地址,再加上偏移量就可以访问数组元素,
对指针而言存储的是指针符号,需要先访问符号p的地址,取符号p的内容,最后取p的内容指向的字符;
定义成指针,以数组访问时

  1. char *p = "abcdefg"; ... p[3]
  2. char a[] = "abcdefg" ... a[3]

两者都可以取得字符“d”,指针p获得字符d的过程
1、取得符号表p的地址,提取存储于此处的指针,
2、把下标所表示的偏移量与指针的值相加,产生一个地址,
3、访问上面这个地址,取得字符;

指针与数组的区别

 

 

定义指针时,编译器并不为指针所指向的对象分配空间,它只是分配指针本身的空间,除非为用字符串常量初始化指针;
char *p = "bread", 在ANSI C中,初始化指针所创建的字符串常量被定义为只读;初始化数组所创建的字符串常量是可以被修改的char a[] = "strawberry"; strncpy(a, "black", 5);
 

Chapter 5

编译系统:预处理器->编译器->汇编器->链接器;
预处理 (C Preprocessor):预处理以字符#开头的命令,修改原始的C程序,得到.i作为文件扩展名;
编译阶段: 将文本文件.i翻译成文本文件.s,它包含一个汇编语言程序,汇编语言为不同的编译器提供了通用的输出语言;
如下所示hello.s

  1. main:
  2.     subq $8, %rsp
  3.     movl $.lCO, %edi
  4.     call puts
  5.     movl $0, %eax
  6.     addq $8, %rsp
  7.     ret

汇编阶段:汇编器将.s翻译成机器语言指令,并打包成可重定位目标程序(relocatable object program),并将结果保存在hello.o中,hello.o文件是一个二进制文件,用文本编辑器打开是一堆乱码;
链接阶段 :hello程序调用了printf函数,它是每个C编译器都提供的标注C库的一个函数。printf函数存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到hello.o中。链接就是把.o文件合并在一起得到hello文件,它是一个可执行目标文件,可以被加载到内存,处理器读取并解释储存在内存中的指令。

GNU环境包括EMACS编辑器,GCC编译器,GDB调试器,处理二进制文件的工具以及其他一些部件,但内核是Linux的;gcc编译器支持多种语言,C,C++,Fortran,Java,Pascal,Object-C

系统的硬件组成:总线,通常总线被设计成传送定长的字节块,也就是字(word),字中的字节数(字长)是一个基本的系统参数,32位机器字长是四个字节,64位机器是8个字节;I/O设备:鼠标,键盘,显示器,磁盘都有一个控制器或适配器与总线相连;主存是一个线性的字节数组,每个字节都有都有其唯一的地址,一般来说程序的每条指令都由不同数量的字节构成;处理器:解释存储在主存中指令的引擎,处理器的核心是一个大小为一个字的存储设备(寄存器),成为程序计数器(PC),任何时候,PC都指向主存中的某条机器语言指令(含有该条指令的地址)。处理器从系统上电到断电一直不断的执行程序计数器指向的指令,再更新程序计数器,使其指令下一条指令。处理器看上去是按照一个非常简单的指令执行模型来操作的,这个模型是指令集架构决定的。定的。

高速缓存存储器,缓解了处理器寄存器和内存之间的读取速度差异,作为暂时的集结区域,存放处理器近期可能会需要的信息。j进程是操作系统对一个正在运行的程序的一种抽象,在一个系统上可以同时运行多个进程;而并发进行则是说一个进程的指令和另一个进程的指令是交错执行的。操作系统实现处理器在进程间切换的机制称为上下文切换。进程到另一个进程是由操作系统内核管理的,内核是操作系统代码常驻主存的部分。一个进程实际上由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。

虚拟内存为每个进程提供了完整的虚拟地址空间,让进程认为它独占内存。进程的虚拟地址空间,

程序被编译成二进制文件=代码段(text)+ 数据段(data)+ Bss段 ;

在运行时程序在内存中分布 = 代码段(text)+ 初始化数据段(data) + 未初始化数据段(Bss) + 堆(heap)+ 栈(stack)
 

 

Bss中存储未初始化的数据,所以data与bss的区别在于bss只存储变量需要占据的内存大小但并不分配内存,要为data分配内存

以上内容可参考博客:

https://blog.csdn.net/YuZhiHui_No1/article/details/38458711

https://blog.csdn.net/gatieme/article/details/43567433

https://blog.csdn.net/K346K346/article/details/45592329

https://blog.csdn.net/love_gaohz/article/details/41310597

https://blog.csdn.net/zhangskd/article/details/6956638

https://blog.csdn.net/acs713/article/details/9055193  ( 字符数组分配在栈中分配内存;而字符串分配在文字常量区(数据区));

指令集并行:现代处理器可以同时执行多条指令的属性成为指令集并行。

当链接一个程序时,需要使用的每个库函数的一份拷贝被加入到可执行文件中。今年来更新的动态链接逐渐被采用。动态链接库提供一个庞大的函数库集合,程序将在运行时寻找需要的函数,而不是把函数库的二进制代码作为自身可执行文件的一部分。
如果函数库的一份拷贝作为可执行文件的物理组成部分,称之为静态链接;如果可执行文件名只是包含了文件名,让载入器在运行时能够寻找程序所需要的函数库,称之为动态链接。执行可执行文件的三个阶段是链接-编辑(link-editing)、载入(loading)和运行时链接(runtime linking);
 静态链接和动态链接
静态链接的模块在链接编辑并载入等待运行,动态链接的模块被链接编辑后载入,在运行时进行链接以便运行。程序执行时,在main()函数被调用之前,运行时载入把共享的数据对象载入到进程的地址空间。外部函数被真正调用之前,运行时载入器并不解析它们。因此即使链接了函数库,如果并没有实际调用,也不会带来额外开销。 
动态链接的主要目的是把程序和它使用的特定的函数库版本分离开来,取而代之的是,约定由系统向程序提供一个接口,并保持稳定,不随时间和操作系统的后续版本发生变化。由于它是介于应用程序和函数库二进制可执行文件所提供的服务之间的接口,所以称为应用程序的二进制接口(Application Binary Interface,ABI)。动态链接必须保证4个特定函数库:libc(C 运行时函数库)libsys(其他系统函数),libX(X windowing)和libnsl(网络服务)。
动态链接的可执行文件体积可以很小,虽然运行速度稍微慢一些,但可以更有效的利用磁盘空间和虚拟内存,因为函数库只有在需要时才被映射到进程中,链接器通过把库文件名或路径名植入到可执行文件来做到这一点,而且链接编辑阶段的时间也会缩短(因为链接器的有些工作被推迟到载入时)。所有动态链接到某个特定函数库的可执行文件在运行时共享该函数库的一个单独拷贝,这就提供了更高的I/O交换和交换空间利用率。
 动态链接是一种just-in-time链接,意味着程序运行时必须能够找到它们所需要的函数库。链接器通过把库文件名或路径名植入到可执行文件中来做到这一点。这意味着,函数库的路径不能随意移动。当在一台机器人编译完成后,拿到另一台机器上运行时可能会出现这种情况。
静态链接扩展名“.a”(archive),动态链接扩展名“.so”(shared object,表示每一个链接到该函数库的程序都共享它的一份拷贝);创建静态或动态的函数库,只需简单地编译一些不包含main函数的代码,并把编译所产生的.o文件用正确的实用工具处理,如果是静态库,实用“ar”,如果是动态库,使用“ld”.
 
参考博客:Unix编译C语言程序----https://blog.csdn.net/ZR_Lang/article/details/17080335

Chapter 6

运行时系统;
a.out是assembler output“汇编程序输出”的缩写形式(由于历史原因),但实际上是链接器输出。
UNIX系统,ELF,Extensible Linker Format 可扩展链接器格式,现在指Executable and Linking
 Format,可执行文件和链接格式;section是ELF文件中最小组织单位,一个段(Segment)一般包含几个
section,段表示一个二进制文件相关的内容块。
size a.out会得到a.out文件中三个段(文本段,数据段和bss段),bss段是Block Started by Symbol(由符号
开始的块),bss字段只保存没有值的变量;
a.out以segment(段)的形式组织,因为段可以方便的映射到运行时链接器直接载入载入的对象中。载入器只是取
文件中每个段的映像,并放入到内存中。
堆栈段在程序执行时用于保存临时数据,局部变量,传递到函数中的参数等,还有malloc分配的动态内存。
过程活动记录,跟踪调用链;
 

Chapter 7

虚拟地址,页
虚拟内存链接:
https://juejin.im/post/59f8691b51882534af254317;
https://www.cnblogs.com/zl1991/p/8193398.html
进程只能操作位于物理内存的页面。当进程引用一个不存在的物理内存页面时,MMU(内存管理单元)产生页错误,内核会判断该引用是否有效。如果无效,内核向进程发出一个"segmentation violation(段违规)"的信号。如果有效内核从磁盘取回该页,换入到内存中。一旦页面进入内存,进程便被解锁,可以重新运行---进程本身并不知道它曾经因为页面换入事件等待了一会
 
访问速度排序,CPU寄存器>Cache存储器>内存>磁盘;虚拟内存以“页”的形式组织。页就是在操作系统在磁盘和内存之间移动的对象,一般大小几K字节。内存的映像在磁盘和物理内存之间来回移动,称为page in(移入内存)或page out(移到磁盘)。虚拟地址按页划分,页映射到物理内存或虚拟内存上,页被加载入内存时MMU(内存管理单元)会将虚地址转换成物理内存地址。
野指针可能会访问没有建立与物理内存映射的虚拟内存。内核空间根据独立且唯一的页表init_mm.pgd进行映射,而用户空间的页表则每个进程维护自己的一份。
 
Cache包含一个地址的列表以及它们的内容,随着CPU不断引用新的内存地址,Cache的地址列表也一直处于变化中。所有对于内存的读取和写入都会经过Cache。当处理器要引用的地址在Cache中,它可以立刻从Cache中获取。否则,Cache就向内存传递这个请求,于是就要较缓慢的访问内存操作。内存读取的数据以行为单位,在读取的同时也转入到Cache中。在UNIX中,内存就是磁盘inode的Cache.在关闭电源前需要把Cache的内容刷新到磁盘,文件系统就有可能损坏。
Cache的访问单位是"行"(line),每行有两部分组成,数据部分以及用于指定它所代表的地址的标签;数据部分被称为“块”(block),块保存来回移动于Cache行和内存之间的字节数据,典型块大小为32字节;一个Cache行的内容代表特定的内存块,如果处理器试图访问属于该块对应范围地址的内存,Cache会立刻做出反应不需要向内存传递请求,这样自然就会块很多。一个Cache(一般为64K到1M之间)由许多行组成。为了提高速度,Cache的位置离CPU很近,而且内存系统和总线经过高度优化,尽可能地提高大小等于Cache块的数据块的移动速度。
 
就像堆栈段能够根据需要内存上的堆区域用于动态内存的分配。堆内存的回收不必与它所分配的顺序一致,所以无序的malloc/free最终会产生堆碎片内存损坏,释放或改写仍在使用的内存;内存泄露:未释放不再使用的内存。
 
函数参数中,数组是作为指针来传递的,所以函数参数中数组和指针形式是可以互换的,譬如main函数中的char **argv和char *argv[]使用快速的左移运算代替缓慢的加法预算;“表达式中的数组名”就是指针;C语言把数组的下标改写成指针偏移量的根本原因是指针和偏移量是底层硬件所使用的基本模型;”作为函数参数的数组名“等同于指针;ANCI C标准规定作为”类型的数组“的形参的声明应该调整为”类型的指针“。在函数形参定义这个特殊情况下,编译器必须把数组形式的形参改写成指向该数组第一个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。为什么C语言把数组形参当做指针?在C语言中,所有非数组形参的数据均以传值形式(对实参做一份拷贝并传给调用的函数,函数不能修改作为实参的实际变量的值,而只能修改传递给它的那份拷贝)调用。如果拷贝整个数组,内存空间和时间上开销都非常大。
 

Chapter 9

  1. void fun1(int *ptr)
  2. {
  3. ptr[1] = 3;
  4. *ptr = 3;
  5. ptr = array2;
  6. }
  7. void fun2(int arr[])
  8. {
  9. arr[1] = 3;
  10. *arr = 3;
  11. arr = array2; //虽然arr申明为数组,但是作为函数形参会被编译器转成指向数组第一个元素的指针
  12. }
  13. int array[100], array2[100];
  14. main()
  15. {
  16. array[1] = 3;
  17. *array = 3;
  18. array = array2;//编译失败,因为array这里就是数组,不等同于指针,数组名作为左值是不能被修改
  19. }
  20. 链接:https://www.cnblogs.com/Howe-Young/p/4160289.html
  21. int (*array)[20] //指针,指向20个元素的int数组
  22. int *array[20] //指针数组,每一个元素指向int类型
  23. int (*paf())[20] {
  24. int (*pear)[20]; /* 声明一个指向包含20个int元素的数组的指针 */
  25. pear = calloc(20, sizeof(int));
  26. if(!pear) longjmp(error, 1);
  27. return pear;
  28. }
  29. //调用函数
  30. int (*result)[20];
  31. result = paf(); //调用函数
  32. (*result)[3] = 12; //访问数组
  33. //定义结构包含数组,让函数返回结构
  34. struct a_tag {
  35. int array[20];
  36. } x,y;
  37. struct a_tag my_function() {
  38. ……
  39. return y;
  40. }
  41. //函数不可以返回指向局部变量的指针,但可以返回局部变量,确切的说,函数不能返回指向调用栈的指针,
  42. //但是可以返回指向调用堆的指针;将局部变量声明为static,变量就保存在数据段而不是调用栈中,该变量
  43. //声明周期与程序一样长,函数退出时,变量的值依然存在

 

Chapter 11

面向对象变成的特点是集成和动态绑定。C++通过类的派生支持集成,通过虚拟函数支持动态绑定。虚拟函数提供了一种封装类体系实现细节的方法。

继承与嵌套不同。嵌套常用于实现容器类(数据结构的类,如链表,散列表,队列等)。C++的模板也用于实现容器类。

C++使用<<操作符(输入)和>>操作符(输出)来替代C语言中的putchar()和getchar()等函数。

<<和>>操作符在C语言中用作左移位和右移位操作符,在C++中被重载用于C++的I/O,编译器查看操作数的类型来决定移位还是I/O代码。

<<和>>操作符可被定义用于任何类型,不需要像C语言一样用字符串格式化限定符%d,并且可以连续输出

例如,cout << "the value is" << i << endl;

但仍然可以使用C++中的stdio.h函数;

函数重载总是在编译时进行解析,编译器查看操作数的类型并决定使用哪种定义,因此重载函数原型必须不同,运算符重载也是函数重载。

多态-----运行时绑定,也成为后期绑定,virtual 成员函数声明多态函数;C++通过覆盖支持(override)机制来实现多态;当使用类继承时就要用到这种机制;virtual 函数的原型必须相同,由运行时系统决定调用哪个函数,基类中用virtual声明,virtual关键字不可以省略

虚拟函数表;

异常(exception):通过发生错误时把自动切换到程序中用于处理错误的那部分代码,从而简化错误处理

模板(template):支持参数化类型。同类型/对象的关系一样,函数/模板的关系也可以看作是为算法提供一种“甜点刀具”的方法,一旦确定了基本的算法就可以用于不同的类型。例如,template<class T> T min(T a, T b) { return  (a<b) ? a : b },允许对min函数和变量a,b赋予任意的类型(该类型必须能接收<操作符)

内联(inline):规定某个函数在行内以指令流的形式展开(就像宏一样),而不是产生一个函数调用。

new 和 delelte操作符,用于取代malloc和free函数。new和delete用来更方便一些(如能够自动完成sizeof的计算工作,并会自动调用合适的构造函数和析构函数),new能够真正地创建一个对象,而malloc只是分配内存。

传引用调用(call by reference,相当于传址调用):C语言只使用传值调用

编程语言有一个特性,成为正交性(orthogonality),是指不同的特性遵循同一个基本原则的程度(也就是学会一种特性有助于学习其他的特性)。

Fortran语言(Formula translation)是第一个高级语言,提供了强大的方法来表达数学公式

C语言设计哲学
一切工作程序员自己负责;
C语言的所有特性都不需要隐式的运行时支持;
程序员所做的都是对的;
程序员应该知道自己在干什么,并保证自己的所作所为是正确。

C++对C语言的改进
C语言中,初始化一个字符数组的方式很容易产生错误,就是数组很可能没有足够的空间存放结尾的NULL字符。
C++对此作了一些改进,像char b[3] = "Bob"这样的表达式被认为是错误的,但是在C语言中是合法的。
类型转换即可以是float(i)这样看上去更顺眼的形式,也可以写成像(float)i这样稍怪异的C语言风格的形式。
C++允许一个常量整数来定义数组的大小,
const int size = 128; 
char a[size];
但是C语言中却是不合法的,C语言声明数组的大小只能用char a[128]或char a[宏定义定义的数字]
C++声明可以穿插于语句之间。在C语言中,一个语句块中的所有声明必须放在所有语句的前面。C++去掉了这个限制。

在C++中存在,但在C语言中却不存在的限制有
C++中用户代码不能调用main()函数,但在C语言中确实允许的;
C++中要求完整的函数原型声明,但是在C语言中却没那么严格;
C++ typedef定义的名字不能与已有的结构标签冲突,但是C语言中却是允许的(它们分别属于不同的名字空间)。

当void *指针赋值给另一个类型的指针时,C++规定必须进行类型转换,但在C语言中却无必要。

在C++和C语言中含义不一样的特性:

C++至少增加了十几个关键字。这些关键字在C语言中可以作为标识符使用,但如果这样做了,用C++编译器编译这些代码就会产生错误信息。

C++,声明可以出现在语句可以出现的任何地方。在C语言代码中块中,所有的声明必须出现在所有语句的前面

C++中一个内层作用域的结构名将会隐藏外层空间相同的对象名。在C语言中则并非如此。
 

 

 

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/160738
推荐阅读
相关标签
  

闽ICP备14008679号