知乎 转载

转载几篇知乎上我自己的回答,因为不喜欢知乎的排版,所以在博客里重新排版一遍。

原问题:C语言中“.”与“->”有什么区别?

除了表达形式有些不同,功能可以说完全一样阿。那为何又要构造两个功能一样的运算符? 效率有差异?可是现在编译器优化都那么强了,如果真是这样岂不是有些多此一举


刚刚翻了下书,说早期的C实现无法用结构直接当作参数在函数间传递,只能用指向结构的指针在函数间进行传递!我想这应该也是最直观的原因吧。

我的回答

首先 a->b 的含义是 (*a).b ,所以他们是不同的,不过的确 -> 可以用 * . 实现,不需要单独一个运算符。 嗯,我这是说现代的标准化的 C 语义上来说, -> 可以用 * . 的组合实现。

早期的 C 有一段时间的语义和现代的 C 的语义不太一样。

稍微有点汇编的基础的同学可能知道,在机器码和汇编的角度来看,不存在变量,不存在 struct …

这篇也是源自于水源C板上板友的一个问题,涉及Linux上的控制台的实现方式和历史原因。因为内容比较长,所以在这里再排版一下发出来。 原帖在这里

可以设置不带缓冲的标准输入流吗?

WaterElement(UnChanged) 于 2014年12月09日23:29:51 星期二 问到:

请问对于标准输入流可以设置不带缓冲吗?比如以下程序

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    FILE *fp = fdopen(STDIN_FILENO, "r");
    setvbuf(fp, NULL, _IONBF, 0);
    char buffer[20];
    buffer[0] = 0;
    fgets(buffer, 20, fp);
    printf("buffer …

这两天在饮水思源的C板,关于C++模板的类型转换的一个讨论,后面是我的解答。

原问题

今天在书上看到模板演绎的时候可以允许cast-down,于是我写了个东西:

template <bool _Test, class _Type = void>
struct enable_if { };

template<class _Type>
struct enable_if<true, _Type> {
    typedef _Type type;
};

class A { };
class B : A { };

template <typename T>
struct …

farseerfc.wordpress.com 导入

3.2 标号、goto,以及switch的实现

goto语句及标号(label)是最古老的C语言特性,也是最早被人们抛弃的语言特性之一。像汇编语言中的jmp指令一样,goto语句可以跳转到同一函数体中任何标号位置:

void f()

{int i=0;

Loop: //A label

++i;

if(i<10)goto Loop; //Jump to the label

}

在原始而和谐的早期Fortran和Basic时代,我们没有if then else,没有for和while,甚至没有函数的概念,一切控制结构都靠goto(带条件的或无条件的)构件。软件工程师将这样的代码称作“意大利面条”代码。实践证明这样的代码极容易造成混乱。

自从证明了结构化的程序可以做意大利面条做到的任何事情,人们就开始不遗余力地推广结构化设计思想,将goto像猛兽一般囚禁在牢笼 …

farseerfc.wordpress.com 导入

3.1 左值右值与常量性(lvalue,rvalue & constant)

首先要搞清楚的是,什么是左值,什么是右值。这里给出左值右值的定义:

1、左值是可以出现在等号(=)左边的值,右值是只能出现在等号右边的值。

2、左值是可读可写的值,右值是只读的值。

3、左值有地址,右值没有地址。

根据左值右值的第二定义,值的左右性就是值的常量性——常量是右值,非常量是左值。比如:

1=1;//Error

这个复制操作在C++中是语法错误,MSVC给出的错误提示为“error C2106: '=' : left operand must be l-value”,就是说’=’的左操作数必须是一个左值,而字面常数1是一个右值。可见,严格的区分左值右值可以从语法分析的角度找出程序的逻辑错误。

根据第二定义,一个左值也是一个右值 …

farseerfc.wordpress.com 导入

2.2 I386平台的内存布局

众所周知,I386是32位体系结构。因此对于绝大多数I386平台的C++编译器而言,sizeof(int)=sizeof(long)=sizeof(void*)=4。当然C++标准对此没有任何保证,我们也不应该试图编写依赖于此的代码。

32位指针的可寻址空间为4GB。为充分利用这么大的寻址空间,也是为了支持其它更先进的技术比如多任务技术或者动态链接库技术,WinNT使用虚拟内存技术,给与每个应用程序全部4GB的内存空间。4GB的地址被一分为二,前2GB供应用程序自己使用,后2GB由系统内核分配和管理。这2GB的内存地址,通常被划分成3种内存区使用:

1 代码及静态数据区

由代码加载器从动态链接库镜像(通常是exe或dll文件)加载,通常定位到镜像文件中指定的基址开始的内存区。如果基址所在内存已被占用,动态连接器会将代码或数据重定向到其它可用地址。

在C++中,静态数据包括:名字空间(namespace)和全局(global)对象、函数的static对象、类的static数据成员 …

farseerfc.wordpress.com 导入

2.3 I386平台C函数内部的栈分配

函数使用栈来保存局部变量,传递函数参数。进入函数时,函数在栈上为函数中的变量统一预留栈空间,将esp减去相应字节数。当函数执行流程途径变量声明语句时,如有需要就调用相应构造函数将变量初始化。当执行流程即将离开声明所在代码块时,以初始化的顺序的相反顺序逐一调用析构函数。当执行流程离开函数体时,将esp加上相应字节数,归还栈空间。

为了访问函数变量,必须有方法定位每一个变量。变量相对于栈顶esp的位置在进入函数体时就已确定,但是由于esp会在函数执行期变动,所以将esp的值保存在ebp中,并事先将ebp的值压栈。随后,在函数体中通过ebp减去偏移量来访问变量。以一个最简单的函数为例:

void f()

{

int a=0; //a的地址被分配为ebp-4

char c=1; //c的地址被分配为ebp-8

}

产生的汇编代码为:

push ebp ;将ebp压栈

mov ebp,esp ;ebp=esp 用栈底备份栈顶指针

sub …

farseerfc.wordpress.com 导入

2.4 I386平台C函数调用边界的栈分配

当调用一个函数时,主调函数将参数以声明中相反的顺序压栈,然后将当前的代码执行指针(eip)压栈,然后跳转到被调函数的入口点。在被调函数中,通过将ebp加上一个偏移量来访问函数参数,以声明中的顺序(即压栈的相反顺序)来确定参数偏移量。被调函数返回时,弹出主调函数压在栈中的代码执行指针,跳回主调函数。再由主调函数恢复到调用前的栈。

函数的返回值不同于函数参数,通过寄存器传递。如果返回值类型可以放入32位变量,比如int、short、char、指针等类型,通过eax寄存器传递。如果返回值类型是64位变量,如_int64,同过edx+eax传递,edx存储高32位,eax存储低32位。如果返回值是浮点类型,如float和double,通过专用的浮点数寄存器栈的栈顶返回。如果返回值类型是用户自定义结构,或C++类类型,通过修改函数签名,以引用型参数的形式传回。

同样以最简单的函数为例:

void f(){

int i …

farseerfc.wordpress.com 导入

2.5 I386平台的边界对齐(Align)

首先提问,既然I386上sizeof(int)==4、sizeof(char)==1,那么如下结构(struct)A的sizeof是多少?

struct A{int i;char c;};

答案是sizeof(A)==8……1+5=8?

呵呵,这就是I386上的边界对齐问题。我们知道,I386上有整整4GB的地址空间,不过并不是每一个字节上都可以放置任何东西的。由于内存总线带宽等等的技术原因,很多体系结构都要求内存中的变量被放置于某一个边界的地址上。如果违反这个要求,重则导致停机出错,轻则减慢运行速度。对于I386平台而言,类型为T的变量必须放置在sizeof(T)的整数倍的地址上,char可以随便放置,short必须放在2的整数倍的地址上,int必须放在4的整数倍的地址上,double必须放在8的整数倍的地址上。如果违反边界对齐要求 …

farseerfc.wordpress.com 导入

2.6 I386平台C函数的可变参数表(Variable Arguments)

基于前文(2.4节)分析,我们可以不通过函数签名,直接通过指针运算,来得到函数的参数。由于参数的压栈和弹出操作都由主调函数进行,所以被调函数对于参数的真实数量不需要知晓。因此,函数签名中的变量声明不是必需的。为了支持这种参数使用形式,C语言提供可变参数表。可变参数表的语法形式是在参数表末尾添加三个句点形成的省略号“...”:

void g(int a,char* c,...);

省略号之前的逗号是可选的,并不影响词法语法分析。上面的函数g可以接受2个或2个以上的参数,前两个参数的类型固定,其后的参数类型未知,参数的个数也未知。为了知道参数个数,我们必须通过其他方法,比如通过第一个参数传递:

g(3,”Hello”,2,4,5);//调用g并传递5个参数,其中后3个为可变参数。

在函数的实现代码中,可以通过2.4节叙述的 …

farseerfc.wordpress.com 导入

2.7 I386平台的其它函数调用模型

上文介绍的只是I386平台上C函数调用的标准模型,被称作__cdecl。事实上,Microsoft Visual C++编译器还支持其它一些函数调用模型,所有调用模型名称皆以双下划线开头,下面列出所有函数调用模型的异同:

1 __cdecl

参数压栈顺序:逆序(从右至左)

参数堆栈恢复者:主调函数(caller)

__cdecl明确地指出函数使用C函数调用模型,这是默认的调用模型。

2 __stdcall

参数压栈顺序:逆序(从右至左)

参数堆栈恢复者:被调函数(callee)

__stdcall是微软所谓的标准调用模型。可惜的是它与__cdecl不兼容。几乎所有的Win32API函数使用这种函数调用模型,希望在DLL之间,或者在程序和WinNT操作系统之间传递函数指针的函数也应该使用这种模型。与__cdecl模型的不同之处在于,__stdcall模型下由被调函数恢复堆栈。主调函数在call语句之后,不需要再加上add语句。而被调函数的ret语句则被添加一个参数,代表函数参数堆栈的长度。因此,被调函数需要明确的知晓函数参数的数量和类型,所以在__stdcall模型下不支持可变参数表,所有参数必须写明 …

farseerfc.wordpress.com 导入

C++ Tricks

By  FarseerFc

从今天起,我再将在Live SpaceQQZone同时发表一系列文章,暂定名为“C++Tricks”。

本文旨在记录和阐述一些本人学习C++时所得的心得、技巧。总体来看,本文涉及的内容是每一个C++程序员都应该知道的,但是很少见诸C++教材。希望对各位同仁学习C++有所帮助。

也可以通过QQ或MSN向我索要此文的DOC版或PDF版,会比网页上的更新的快一点。

1      词法问题(Lexical Problems)


2      X86体系结构

     2.1 X86概述

     2.2 …