有对printf 实现函数实现熟悉的没

====================================================================
我注意到有很多人通过关键字"printf 多线程"搜索到了我的这篇文章,我也意识到有很多Linux和Windows程序员在编写多线程程序中使用printf导致的种种问题。我猜测这与printf设计时的不可重入性有关,虽然我在文中用嵌入式的串口打印函数作为类比,可是这两个或许还不是一回事。
如果你遇到了这个问题,你可以尝试以下方法,即使用多线程运行时库(Mulit-Thread Runtime Library):
Project-& Settings-& C/C++-& Code
Generation-& use
如果希望在多线程中使用,那么要选择multithreaded dll
当然如果你只是作为调试,可以使用TRACE宏。
2、Linux环境
Linux环境中如何设置运行时库的参数我不是非常清楚,不过只要保证调用的是多线程运行时库即可。可是,Linux会有这个问题么?
3、更通用的方法
更通用的办法就是使用互斥锁、信号量等方法,不过这可能会改变程序运行的本意。
以下才是原文的正文,不过如果你只是来解决以上问题的,本文就对你没有价值了:
====================================================================
本文作者:云飞工作室(YunFei Studio),戴晓天
联系方式:Automatic.
原文地址:
一、本文背景
Printf()这个函数我想大家再熟悉不过了,可是对于如何在多线程中使用Printf(),各位可能就没怎么接触过了。本文以VC6.0为开发平台,旨在利用多线程完成一个最简单的任务:在屏幕上一直输出"Hello,world!"。
因为个人水平有限,本文涉及的内容非常肤浅,不过我还是尽量将原理讲清楚,希望对初学者略有帮助。
为了提高大家的兴趣、降低理解难度,文章的结构也是几经修改。考虑到本文阅读者可能对于多线程不怎
么了解,本文将首先介绍Win32多线程编程。
二、多线程概述
2.1 进程与线程
在操作系统中,一个进程就是一个程序的实例。当你双击打开一个程序之后,实际上就启动了一个进程。该进程将程序代码装载至RAM,并运行、管理该实例。而线程是进程的最小分割,好比运行一个盖楼房的程序进程,需要各有分工,有的人打地基,有的人刷水泥,有的人搬材料一样。这里,每一个人都是一个线程。相比单线程(一个人盖楼房),多线程(几个人配合盖楼房)可以显著提高系统运行的效率。线程彼此独立工作,却有相关性,比如打地基的要等搬材料的把材料运过来,才可以施工。这就说明,线程之间有时需要彼此配合完成任务,这就需要进程间通讯(IPC)。
2.2 Windows多线程编程
在Windows下使用多线程,实际上就是通过调用Windows提供的应用程序接口API来完成特定任务。其中,最基本的是创建一个多线程,该函数的原型为:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
// pointer to security attributes
DWORD dwStackSize,
// initial thread stack size
LPTHREAD_START_ROUTINE lpStartAddress,
// pointer to thread function
LPVOID lpParameter,
// argument for new thread
DWORD dwCreationFlags,
// creation flags
LPDWORD lpThreadId
// pointer to receive thread ID
lpThreadAttributes决定是否能被子线程继承,一般为NULL;
DwStackSize决定任务的堆栈大小,一般设为0,既由操作系统管理;
lpStartAddress是一个函数指针,填写线程函数名称;
lpParameter是传递给线程的参数;
dwCreationFlags是线程的创建属性,可设置为CREATE_SUSPENDED,即初始化时挂起;
lpThreadId决定线程的ID号。
一个线程实际上就是一个函数的实例,该线程拷贝了线程函数的代码而执行,所以多个线程可以运行同一个线程函数。使用相同线程函数的不同线程,可以通过传递不同的lpParameter来决定其各自的特殊任务。线程函数的原型如下:
DWORD WINAPI ThreadProc(
LPVOID lpParameter
// thread data
WINAPI表示这是一个WINAPI函数,ThreadProc是函数名,可任意修改,lpParameter为创建线程时传递的参数,实际上为VOID *型。
三、Printf你怎么了
有了以上的知识,我们现在可以开始完成我们的任务:利用多线程完成最经典的"Hello,world!"程序。在这里,我们创建两个任务:Thread1_Proc()和Thread2_Proc(),其中Thread1_Proc()用于完成输出"Hello,",Thread2_Proc()用于完成输出"World!\n"。
首先,我们打开VC6.0,创建一个Win32 Console程序。打开main.c,在开始处添加:
#include &windows.h&
这是因为,我们需要使用到Windows的API函数。
在main()函数中,新建两个线程:
int main(int argc, char* argv[])
DWORD nID;
CreateThread(NULL, 0, Thread1_Proc, NULL, 0, NULL);
CreateThread(NULL, 0, Thread2_Proc, NULL, 0, NULL);
在创建完线程后,main函数无限循环。线程函数的形式如下:
DWORD WINAPI Thread1_Proc(PVOID pvParam)
printf(&Hello,&);
Sleep(50);
DWORD WINAPI Thread2_Proc(PVOID pvParam)
printf(&World!\n&);
Sleep(50);
点击"编译全部",之后按F5进入调试模式。
出现了如下的结果:
乍一眼看上去,好像没什么问题啊。不过再仔细看看,大部分结果都是不正确的,有时候几个Hello之后才有一个World,有时Hello和World交叉在了一起,变成了HlWelloorHld。这是怎么回事呢?我们熟悉的Printf怎么变成了这个样子。
四、Printf为何不听话
Printf是大家学习C语言时最常使用的输出语句,但所谓越好用的东西也是越难懂的东西。一个小小的Printf,暗藏多少杀机。考虑到Windows实现的复杂性,我们用一个Keil下的Printf驱动来说明问题:
int _sys_write (FILEHANDLE fh, const U8 *buf, U32 len, int mode) {
if (fh == STDOUT) {
for ( len--) {
sendchar (*buf++);
return (0);
int sendchar (int ch) {
if (ch == '\n') {
while (!(USART1-&SR & USART_FLAG_TXE));
USART1-&DR = '\r';
while (!(USART1-&SR & USART_FLAG_TXE));
return (USART1-&DR = (ch & 0x1FF));
该函数的实现分为两部分,第一部分是发送字符串int_sys_write(),第二部分是发送单个字符sendchar(),第一部分依赖第二部分实现。所以实际当输出Hello,world时,不是一起发送出去的,而是一位接一位的交给标准输出:H-e-l-l-o-,-w-o-r-l-d-!。所以如果正在发送过程中插入了其他发送请求,就会出现输出紊乱的现象。
五、如何让Printf乖乖听话
实际上这个问题的关键就是资源。无论Printf的输出对象是屏幕还是串口,同一时刻只能有一个线程访问该资源。为了对这个资源加以控制,避免两个线程同时访问,就要使用线程间通讯(IPC)技术。在这里,我们使用临界段技术,所谓临界段就是规定一个区域,在有线程占用这个区域时,其他线程就不能够访问。修改后的线程代码为:
DWORD WINAPI Thread1_Proc(PVOID pvParam)
EnterCriticalSection(&cs);
printf(&Hello,&);
LeaveCriticalSection(&cs);
Sleep(50);
再一次编译-运行,好了,这下总该正常了吧?其实还没有:
从图中可以看到,经过修改后的程序不会再出现Hello和World交错的问题,但是却并不是挨个输出,有时一个Hello要有多个World。这是因为虽然资源的问题解决了,但是两个任务间却还没有同步。
这里,我只简单的使用了全局标志来实现互锁,其他实现方法,如EVENT读者可以自己探究。
完整的代码如下:
#include &stdafx.h&
#include &windows.h&
CRITICAL_SECTION
DWORD WINAPI Thread1_Proc(PVOID pvParam)
while (flag==1);
EnterCriticalSection(&cs);
printf(&Hello,&);
LeaveCriticalSection(&cs);
Sleep(50);
DWORD WINAPI Thread2_Proc(PVOID pvParam)
while (flag==0);
EnterCriticalSection(&cs);
printf(&World!\n&);
LeaveCriticalSection(&cs);
Sleep(50);
int main(int argc, char* argv[])
InitializeCriticalSection(&cs);
CreateThread(NULL, 0, Thread1_Proc, NULL, 0, NULL);
CreateThread(NULL, 0, Thread2_Proc, NULL, 0, NULL);
DeleteCriticalSection(&cs);
六、Printf 在uCOSII中
对于所有的多任务系统,理论上都存在以上问题。在uCOSII中,为了避免以上问题的出现,专门使用了Mutex互斥量来解决资源抢占的问题。有兴趣的读者可以参考uCOSII的相关书籍。
随着电子技术的不断发展,处理器的性能越来越高,即使是单片机也在逐渐告别裸奔的时代。所以了解多任务系统,了解多线程编程与传统编程的区别,是非常重要且迫切的。Printf是最基本的一个函数,如何去用,何时能用,大家都要心中有数,才不至于在小沟处翻大船。
参考资料:
MSDN Library Visual Studio 6.0, Microsoft Corp, 2000
Related Posts
Like this:Like Loading...
WP Cumulus Flash tag cloud by
9 or better.
Recent Posts
%d bloggers like this:1284人阅读
&&&&&&&&&&& 5.
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:6872次
排名:千里之外一本书的前言 “除非在特殊情况下,不允许用printf 这个函数”_c语言吧_百度贴吧
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&签到排名:今日本吧第个签到,本吧因你更精彩,明天继续来努力!
本吧签到人数:0成为超级会员,使用一键签到本月漏签0次!成为超级会员,赠送8张补签卡连续签到:天&&累计签到:天超级会员单次开通12个月以上,赠送连续签到卡3张
关注:514,545贴子:
一本书的前言 “除非在特殊情况下,不允许用printf 这个函数”收藏
觉得它反传统,但是又很有道理的样子
缺牙要及时修复,揭秘种植牙如何做到几十年不掉?
我遇到过很多程序员和计算机系毕业的学生,也给很多程序员和计算机系毕业的学生讲 解过《高级C 语言程序设计》。每期班开课前,我总会问学生:你感觉C 语言学得怎么样? 难吗?指针明白吗?数组呢?内存管理呢?往往学生回答说:感觉还可以,C 语言不难,指 针很明白,数组很简单,内存管理也不难。一般我会再问一个问题:通过这个班的学习, 你想达到什么程度?很多学生回答:精通C 语言。我告诉他们:我很无奈,也很无语。因 为我完全在和一群业余者或者是C 语言爱好者在对话。你们大学的计算机教育根本就是在 浪费你们的时间,念了几年大学,连C 语言的门都没摸着。现在大多数学校计算机系都开 了C、C++、Java 、C#等等语言,好像什么都学了,但是什么都不会,更可悲的是有些大学 居然取消了C 语言课程,认为其过时了。我个人的观点是“十鸟在林,不如一鸟在手”,真 正把C 语言整明白了再学别的语言也很简单,如果C 语言都没整明白,别的语言学得再好 也是花架子,因为你并不了解底层是怎么回事。当然我也从来不认为一个没学过汇编的人 能真正掌握C 语言的真谛。我个人一直认为,普通人用C 语言在3 年之下,一般来说,还 没掌握C 语言;5 年之下,一般来说还没熟悉C 语言;10 年之下,谈不上精通。所以,我 告诉我的学生:听完我的课,远达不到精通的目标,熟悉也达不到,掌握也达不到。那能 达到什么目标?-----领你们进入C 语言的大门。入门之后的造化如何在于你们自己。不过我 可以告诉你们一条不是捷径的捷径:把一个键盘的F10 或F11 按坏,当然不能是垃圾键盘。
往往讲到这里,学生眼里总是透露着疑虑。C 语言有这么难吗?我的回答是:不难。但 你就是用不明白。学生说:以前大学老师讲C 语言,我学得很好。老师讲的都能听懂,考 试也很好。平时练习感觉自己还不错,工作也很轻松找到了。我告诉学生:听明白,看明 白不代表你懂了,你懂了不代表你会用了,你会用了不代表你能用明白,你能用明白不代 表你真正懂了!什么时候表明你真正懂了呢?你站在我这来,把问题给下面的同学讲明白, 学生都听明白了,说明你真正懂了。否则,你就没真正懂,这是检验懂没懂的唯一标准。 冰山大家都没见过,但总听过或是电影里看过吧?如果你连《泰坦尼克》都没看过,那你 也算个人物(开个玩笑)。《泰坦尼克》里的冰山给泰坦尼克造成了巨大的损失。你们都是 理工科的,应该明白冰山在水面上的部分只是总个冰山的1/8。我现在就告诉你们,C 语言 就是这座冰山。你们现在仅仅是摸到了水面上的部分,甚至根本不知道水面下的部分。我 希望通过我的讲解,让你们摸到水面下的部分,让你们知道C 语言到底是什么样子。
从现在开始,除非在特殊情况下,不允许用printf 这个函数。为什么呢?很多学生写完 代码,直接用printf 打印出来,发现结果不对。然后就举手问我:老师,我的结果为什么不 对啊?连调试的意识都没有!大多数学生根本就不会调试,不会看变量的值,内存的值。 只知道printf 出来结果不对,却不知道为什么不对,怎么解决。这种情况还算好的。往往很 多时候printf 出来的结果是对的,然后呢,学生也理所当然的认为程序没有问题。是这样吗? 往往不是,往后看,你能看到例子的。永远给我记住一点:结果对,并不代表程序真正没 有问题。所以,以后尽量不要用printf 函数,要去看变量的值,内存的值。当然,在我们目 前的编译器里,变量的值,内存的值对了就代表你程序没问题吗?也不是,往后,你也会 看到例子的。
这个时候呢,学生往往会莫名其妙。这个老师有问题吧。大学里我们老师都教我们怎么 用printf ,告诉我们要经常用printf 。这也恰恰是大学教育失败的地方之一。很多大学老师根 本就没真正用C 语言写过几行代码,更别说教学生调试代码了。不调试代码,不按F10 或F11 , 水平永远也无法提上来,所以,要想学好一门编程语言,最好的办法就是多调试。你去一 个软件公司转转,去看人家的键盘,如果发现键盘上的F10 或F11 铮亮铮亮,毫无疑问, 此机的主人曾经或现在是开发人员(这里仅指写代码的,不上升到架构设计类的开发人员), 否则,必是非开发人员。
非常有必要申明,本人并非什么学者或是专家,但本人是数学系毕业,所以对理论方面 比较擅长。讲解的时候会举很多例子来尽量使学生明白这个知识点,至于这些例子是否恰 当则是见仁见智的问题了。但是一条,长期的数学训练使得本人思维比较严谨,讲解一些 知识点尤其是一些概念性原理性的东西时会抠的很细、很严,这一点相信读者会体会得到 的。本书是我平时讲解C 语言的一些心得和经验,其中有很多我个人的见解或看法。经过 多期培训班的实践,发现这样讲解得比较透彻,学生听得明白。很多学生听完课后告诉我: 我有生以来听课从来都没有听得这么透彻,这么明白过。也有业余班的学生甚至辞掉本职 工作来听我的课的。
当然,关于C 语言的这么多经验和心得的积累并非我一人之力。借用一句名言:我只 不过是站在巨人的肩膀上而已。给学生做培训的时候我参考得比较多的书有:Kernighan & Ritchie 的《The C Programming Language》;Linden 的《Expert C Programming》;Andrew & Koening 《C Traps and Pitfalls》; Steve Maguire 的《Write Clean Code》;Steve McConnell 的
《Code Complete. Second Edition》;林锐的《高质量C++/C 编程指南》。这些书都是经典之 作,但却都有着各自的缺陷。读者往往需要同时阅读这些书才能深刻的掌握某一知识点。 我的讲课的试图时候融各家之长,再加上我个人的见解传授给学生。还好,学生反映还可 以,至少还没有出乱子。这些书饱含着作者的智慧,每读一遍都有不同的收获,我希望读 者能读上十遍。另外,在编写本书时也参考了网上一些无名高手的文章,这些高手的文章 见解深刻,使我受益匪浅。这里要感谢这些大师们,如果不是他们,肯怕我的C 语言的水 平也仅仅是入门而已。
学习C 语言,这几本书如果真正啃透了,水平不会差到哪。与其说本书是我授课的经 验与心得,不如说本书是我对这些大师们智慧的解读。本书并不是从头到尾讲解C 语言的 基础知识,所以,本书并不适用于C 语言零基础的人。本书的知识要比一般的C 语言书说 讲的深的多,其中有很多问题是各大公司的面试或笔试题。所以本书的读者应该是中国广 大的计算机系的学生和初级程序员。如果本书上面的问题能真正明白80%,作为一个应届 毕业生,肯怕没有一家大公司会拒绝你。当然,书内很多知识也值得计算机教师或是中高 级程序员参考。尤其书内的一些例子或比方,如果能被广大教师用于课堂,我想对学生来 说是件非常好的事情。有人说电影是一门遗憾的艺术,因为在编辑完成之后总能或多或少 的发现一些本来可以做得更好的缺陷。讲课同样也如此,每次讲完课之后总能发现自己某 些地方或是没有讲到,或是没能讲透彻或是忘了举一个轻浅的例子等等。整理本书的过程 也是,为了尽量精炼,总是犹豫一些东西的去留。限于作者水平,书中难免有些遗漏甚至 错误,希望各位读者能予指教。作者Mail:dissection_.
2008 年6 月23
好文章!!!不用printf函数是叫我们多调试一下程序,多实践一下。我对此感悟也比较深,会看代码,不代表会写代码,会写代码,不代表不会出错,不出错,不代表没问题。借用小甲鱼的话是:编程高手是调试出来的
F10和F11是什么,能吃么?
呵呵,在关键点加上printf输出必要信息效率远高好嘛怕是写这书的连用#ifdef加上调试开关都不会告诉你吧
一律用printf的路过.包括图形界面代码
我得键盘f5已近被我用坏了,什么水平
弱爆了。不printf也不调试直接人肉静态分析才是王道(适应性最好,有些地方没法printf也没法断点单步)。“真 正把C 语言整明白了再学别的语言也很简单,如果C 语言都没整明白,别的语言学得再好 也是花架子,因为你并不了解底层是怎么回事。”从这句就可以看出有多业余了。学完C然后顺便干掉一堆FP语言么。非要了解底层能“整明白”C,究竟是对自己对C的认识有多没信心?正告初学者:C里面有相当的东西是没法靠了解底层整明白的,因为是有意和底层隔离的人为的约定。
陈正冲《C语言深度解剖——解开程序员面试笔试的秘密》粗略瞄了几眼,似乎吐槽空间很大的样子。槽点多的话我新开个主题好了。
网狐 专业棋牌游戏开发
实时吐槽顺便给兰州送经验算了。“非常有必要申明,本人并非什么学者或是专家,但本人是数学系毕业,所以对理论方面比较擅长。讲解的时候会举很多例子来尽量使学生明白这个知识点,至于这些例子是否恰当则是见仁见智的问题了。但是一条,长期的数学训练使得本人思维比较严谨,讲解一些知识点尤其是一些概念性原理性的东西时会抠的很细、很严,这一点相信读者会体会得到的。”好吧,拭目以待。
目录就有槽点了。。。1.6.1,bool变量与“零值”进行比较..............................................20
第一章 关键字
每次讲关键字之前,我总是问学生: C语言有多少个关键字?sizeof怎么用?它是函数吗?有些学生不知道 C语言有多少个关键字,大多数学生往往告诉我 sizeof是函数,因为它后面跟着一对括号。当投影仪把这 32个关键字投到幕布上时,很多学生表情惊讶。有些关键字从来没见过,有的惊讶 C语言关键字竟有 32个之多。更有甚者,说大学老师告诉他们 sizeof是函数,没想到它居然是关键字!由此可想而知,大学的计算机教育是多么失败!表(1.1)C语言标准定义的 32个关键字关键字意义 写作时间应该是这个世纪了,当时正式标准是C99且取消了C90,这里却使用C90的内容,对标准版本只字未提。
补充楼上。Reference:ISO/IEC Forward5 This second edition cancels and
replaces the first edition, ISO/IEC , asamended and
corrected by ISO/IEC 9899/COR1:1994, ISO/IEC 9899/AMD1:1995,
and ISO/IEC 9899/COR2:1996. ...
哪个是定义?哪个是声明?或者都是定义或者都是声明?我所教过的学生几乎没有一人能回答上这个问题。这个十分重要的概念在大学里从来没有被提起过!这段说得倒是不错,虽然很可悲。槽点在下面:
什么是定义:所谓的定义就是 (编译器)创建一个对象,为这个对象分配一块内存并给它取上一个名字,这个名字就是我们经常所说的变量名或对象名。但注意,这个名字一旦和这块内存匹配起来(可以想象是这个名字嫁给了这块空间,没有要彩礼啊。^_^),它们就同生共死,终生不离不弃。并且这块内存的位置也不能被改变。一个变量或对象在一定的区域内(比如函数内,全局等)只能被定义一次,如果定义多次,编译器会提示你重复定义同一个变量或对象。函数定义哭了。Tentative definition死有余辜。
什么是声明:有两重含义,如下:
第一重含义:告诉编译器,这个名字已经匹配到一块内存上了(伊人已嫁,吾将何去何从?何以解忧,唯有稀粥),下面的代码用到变量或对象是在别的地方定义的。声明可以出现多次。
不算胡扯也至少应该是语焉不详。这里内存只能讲成实现(比如编译器)在解释语义时的内存才能勉强说得通。
(↓讲static)1.3.1,修饰变量
第一个作用:修饰变量。变量又分为局部和全局变量,但它们都存在内存的静态区。
静态全局变量,作用域仅限于变量被定义的文件中,其他文件即使用 extern声明也没法使用他。准确地说作用域是从定义之处开始,到文件结尾处结束,在定义之处前面的那些代码行也不能使用它。想要使用就得在前面再加 extern ***。恶心吧?要想不恶心,很简单,直接在文件顶端定义不就得了。 非得把file scope说成global的习惯性胡扯。后面那个是声明和定义不分,自己打脸。
静态局部变量,在函数体里面定义的,就只能在这个函数里用了,同一个文档中的其他函数也用不了。由于被 static修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值。搭配不当。好在应该不容易引起误会。
1.3.2,修饰函数第二个作用:修饰函数。函数前加 static使得函数成为静态函数。但此处 “static”的含义不是指存储方式,而是指对函数的作用域仅局限于本文件 (所以又称内部函数)。使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件中的函数同名。严重胡扯。居然能把链接决定的可见性说成作用域,好生佩服。且不说文件和翻译单元不分的问题,要知道作用域本身就是一段程序文本区域,C最广的作用域就是文件作用域,那又如何让作用域不局限于本文件?
后面一堆规则是一家之言。没必要照搬。有意思的是在例子中使用了//。真不知道是讲哪个版本的C了。
【规则 1-14】定义变量的同时千万千万别忘了初始化。定义变量时编译器并不一定清空了
这块内存,它的值可能是无效的数据。虽然说的大致是没错,但遣词有教唆侥幸心理之嫌。恐怕写这话的自己都没底。
这个问题在内存管理那章有非常详细的讨论,请参看。继续拭目以待。1.4,signed、unsigned 关键字我们知道计算机底层只认识0、1.任何数据到了底层都会变计算转换成0、1.那负数怎么存储呢?肯定这个“-”号是无法存入内存的,怎么办?很好办,做个标记。把基本数据类型的最高位腾出来,用来存符号,同时约定如下:最高位如果是1,表明这个数是负数,其值为除最高位以外的剩余位的值添上这个“-”号;如果最高位是0,表明这个数是正数,其值为除最高位以外的剩余位的值。这样的话,一个32位的signed int类型整数其值表示法范围为:- 2^31~2^31 -1;8 位的char类型数其值表示的范围为- 2^7~2^7 -1。一个32位的unsigned int类型整数其值表示法范围为:0~ 2^32 -1;8位的char类型数其值表示的范围为0~2^8 -1。同样我们的signed 关键字也很宽恒大量,你也可以完全当它不存在,编译器缺省默认情况下数据为signed 类型的。说笑呢?1.您到底是想说用反码还是补码表示负数?2.谁告诉你char就是无符号整数?
我们知道在计算机系统中,数值一律用补码来表示(存储)。这里倒是提到补码了。可惜,事实是光是C就支持原码反码补码表示负数:ISO C11(N1570)6.2.6.2 Integer types2 For signed integer types, the bits of the object representation shall be divided into three groups: value bits, padding bits, and the sign bit. There need notsigned char shall not have any padding bits. There shall be exactly one sign bit.Each bit that is a value bit shall have the same value as the same bit in the object representation of the corresponding unsigned type (if there are M value bits in the signed type and N in the unsigned type, then M ≤ N). If the sign bit is zero, it shall not affect the resulting value. If the sign bit is one, the value shall be modified in one of the following ways:— the corresponding value with sign bit 0 is negated (sign and magnitude);— the sign bit has the value −(2M) (two’s complement);— the sign bit has the value −(2M − 1) (ones’ complement).Which of these applies is implementation-defined, as is whether the value with sign bit 1 and all value bits zero (for the first two), or with sign bit and all value bits 1 (for ones’complement), is a trap representation or a normal value. In the case of sign and magnitude and ones’ complement, if this representation is a normal value it is called a negative zero.
不巧我也看过这本书,同样被其中的理论搞晕过。
风格之类废话略过。非常重要的一点是,把正常情况的处理放在if 后面,而不要放在else 后面。当然这也符合把正常情况的处理放在前面的要求。【规则1-18】确保if 和else 子句没有弄反。这一点初学者也容易弄错,往往把本应该放在if 语句后面的代码和本应该放在else 语句后面的代码弄反了。无稽之谈。记住:case 后面只能是整型或字符型的常量或常量表达式(想想字符型数据在内存里是怎么存的)。舍近求远、画蛇添足。整数常量表达式包括字符型常量。
所以,无论在C 还是C++中,若函数不接受任何参数,一定要指明参数为void。扯蛋。【规则1-35】千万小心又小心使用void 指针类型。按照ANSI(American National Standards Institute)标准,不能对void 指针进行算法操作不知道是不是“算术”的笔误。(char *)pvoid++; //ANSI:正确;GNU:正确(char *)pvoid += 1; //ANSI:错误;GNU:正确老底暴露无遗。GNU C什么时候让non lvalue放在+=左边然后编译过了?【规则1-36】如果函数的参数可以是任意类型指针,那么应声明其参数为void *。函数指针哭了。return (Val);//此括号可以省略。但一般不省略,尤其在返回一个表达式的值时。反正我见过的省略的远远多于不省略的。也许是我古董代码看得不够多?至于extern“C”的用法,一般认为属于C++的范畴,这里就先不讨论。当然关于extern的讨论还远没有结束,在指针与数组那一章,你还会和它亲密接触的。本来就是,C哪来的language linkage。
1.14.1,空结构体多大?结构体所占的内存大小是其成员所占内存之和(关于结构体的内存对齐,请参考预处理那章)。这点很容易理解,但是下面的这种情况呢?struct student{}sizeof(stu)的值是多少呢?在Visual C++ 6.0 上测试一下。很遗憾,不是0,而是1。为什么呢?你想想,如果我们把struct student 看成一个模子的话,你能造出一个没有任何容积的模子吗?显然不行。编译器也是如此认为。编译器认为任何一种数据类型都有其大小,用它来定义一个变量能够分配确定大小的空间。既然如此,编译器就理所当然的认为任何一个结构体都是有大小的,哪怕这个结构体为空。那万一结构体真的为空,它的大小为什么值比较合适呢?假设结构体内只有一个char 型的数据成员,那其大小为1byte(这里先不考虑内存对齐的情况).也就是说非空结构体类型数据最少需要占一个字节的空间,而空结构体类型数据总不能比最小的非空结构体类型数据所占的空间大吧。这就麻烦了,空结构体的大小既不能为0,也不能大于1,怎么办?定义为0.5个byte?但是内存地址的最小单位是1 个byte,0.5 个byte 怎么处理?解决这个问题的最好办法就是折中,编译器理所当然的认为你构造一个结构体数据类型是用来打包一些数据成员的,而最小的数据成员需要1 个byte,编译器为每个结构体类型数据至少预留1 个byte的空间。所以,空结构体的大小就定位1 个byte。还是扯蛋。不算扩展,C里面用空结构体就是错的。C++里面才能放心用。错误原因:不算C11的static assert declaration(考虑这点可能是标准草案的defect,因为只有static assert declaration作为成员声明可以绕过这个语法限制,但根据6.2.6.1/2,大小又不可能为0,这样就真成了字面上的“未定义”的了),语法不允许省略成员;如果用bit-field定义无名成员制造“空”结构体则UB。详细解释可以参考:/questions/1626446/what-is-the-size-of-an-empty-struct-in-c
第二章讲所谓的“符号”,看起来其实是在讲标点、操作符和加上一些其它上下文对组成标点的字符的使用(如注释、转义字符)。2.7,++、--操作符这绝对是一对让人头疼的兄弟。先来点简单的:int i = 3;(++i)+(++i)+(++i);表达式的值为多少?15 吗?16 吗?18 吗?其实对于这种情况,C语言标准并没有作出规定。有点编译器计算出来为18,因为i 经过3 次自加后变为6,然后3 个6 相加得18;而有的编译器计算出来为16(比如Visual C++6.0),先计算前两个i 的和,这时候i 自加两次,2 个i 的和为10,然后再加上第三次自加的i 得16。其实这些没有必要辩论,用到哪个编译器写句代码测试就行了。但不会计算出15 的结果来的。事实是ISO C间接但明确规定是错的,或至少不是可移植的C代码。第三章讲预处理,也就是泛泛而谈,加了些VC++ specific的内容,麻烦的玩意儿一点也不深入,懒得找槽点了。第四章讲指针……看来又可以收割了……
上来就讲指针的内存布局。。。他喵的谁告诉你指针一定就是用一个地址实现的?注意NULL 就是NULL,它被宏定义为0:#define NULL 0事实上,因为空指针常量的类型以及隐式转换规则的原因,C标准库实现中一般是(void*)0而不是0。C++倒可以是0。擦……指针的指针都不讲居然就当讲完了?说好的const呢?简单而言,出现在赋值符“=”右边的就是右值,出现在赋值符“=”左边的就是左值。比如,x=y。连仅限于解释名字的含义都不吱一声,还不如不说。左值:在这个上下文环境中,编译器认为x 的含义是x 所代表的地址。这个地址只有编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址,我们完全不必考虑这个地址保存在哪里。关键的“存储”和“对象”不提,倒用莫须有的“地址”忽悠过去了。更大的问题是,真有需要保存这个地址?右值:在这个上下文环境中,编译器认为y 的含义是y 所代表的地址里面的内容。这个内容是什么,只有到运行时才知道。当常量表达式干什么吃的?。a 作为右值时其意义与&a[0]是一样,代表的是数组首元素的首地址,而不是数组的首地址。这是两码事。内存布局决定就地址而言是一码事。指向数组首个元素的指针和指向数组的指针倒是两码事。a 作为右值,我们清楚了其含义,那作为左值呢?类型都不提一下,还真好意思。a 不能作为左值!这个错误几乎每一个学生都犯过。编译器会认为数组名作为左值代表的意思是a 的首元素的首地址,但是这个地址开始的一块内存是一个总体,我们只能访问数组的某个元素而无法把数组当一个总体进行访问。所以我们可以把a[i]当左值,而无法把a当左值。其实我们完全可以把a 当一个普通的变量来看,只不过这个变量内部分为很多小块,我们只能通过分别访问这些小块来达到访问整个变量a 的目的。预料之中的胡扯。明明还试过sizeof和&来着,算打脸么?这就与市面上的C 语言的书有关,几乎没有一本书把这个问题讲透彻,讲明白了。对正经书视而不见,这是什么精神。。。你可以试试用printf("%x",&a+1);打印其值,看是否为0x*sizeof(int)。注意如果你用的是printf("%d",&a+1);打印,那你必须在十进制和十六进制之间换算一下,不要冤枉了编译器。好好的%p放置play,偏偏要自找麻烦么。另外我要强调一点:不到非不得已,尽量别使用printf 函数,它会使你养成只看结果不问为什么的习惯。比如这个列子,*(a+1)和*(ptr-1)的值完全可以通过Watch 窗口来查看。这个还是重复吐槽一遍好了。看Watch不见得会收获比printf多的优越感。
甚至有的书还在讲TruboC 2.0 之类的调试器!如此教材教出来的学生质量可想而知。五十步笑百步。在Visual C++6.0 上给出如下警告:warning C4047: 'initializing' : 'char (*)[5]' differs in levels of indirection from 'char *'。还好,这里虽然给出了警告,但由于&a 和a 的值一样,而变量作为右值时编译器只是取变量的值,所以运行并没有什么问题。不过我仍然警告你别这么用。值包括类型。另外,沿用ISO C的习惯,右值指的就是值。多维数组和多级指针……好吧你赢了,靠布局就混过去了……我们知道p2 是main 函数内的一个局部变量,它只在main 函数内部有效。(这里需要澄清一个问题:main 函数内的变量不是全局变量,而是局部变量,只不过它的生命周期和全局变量一样长而已。全局变量一定是定义在函数外部的。初学者往往弄错这点。)、“全局”“变量”懒得吐槽了。还真敢说main里面局部对象和外面的生存期一样?函数指针倒是举了些无聊的例子,也懒得吐槽。讲内存管理,不讲存储类直接讲实现么。没有漏讲避免重复释放,看来比林锐聪明点。讲得很浅,连realloc都没提。
【规则6-25】函数的功能要单一,不要设计多用途的函数。微软的Win32 API就是违反本规则的典型,其函数往往因为参数不一样而功能不一,导致很多初学者迷惑。【建议6-28】避免函数有太多的参数,参数个数尽量控制在4个或4个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。微软的Win32 API就是违反本规则的典型,其函数的参数往往七八个甚至十余个。比如一个CreateWindow函数的参数就达11个之多。脸打得好。看到这里,也许有人会说,strlen 函数这么简单,有什么好讨论的。是的,我相信你能熟练应用这个函数,也相信你能轻易的写出这个函数。但是如果我把要求提高一些呢:不允许调用库函数,也不允许使用任何全局或局部变量编写intmy_strlen (char *strDest);strDest中枪。后面似乎没什么干货了。总体而言,看来是比谭X要好得多,不过显然对不起“C 语言深度解剖”和“以含金量勇敢挑战国内外同类书籍”的名头。
我觉得作者在说不允许使用printf是有哗众取宠的嫌疑。
我cb是f9重么破
表示,那文章作者也没写过大项目吧?直接用开发工具的调试有时候很好,可是,如果要是需要n个可执行程序协作的时候,特别是有网络通信的时候,你把一个程序中断了另外几个程序就可能触发错误的行为。。而且,像我们现在的项目一个项目几万行代码(linux下的),想去一行行的单步调试吧或者断点调试你自己可以试试,你可能断点执行了几千次都没问题,下一次时出core挂掉了。一般还是要做好以下几件事才好:一、写好单测二、打好日志(类似于printf)三、注意编译器警告,或者直接-Wall编译四、用valgrind跑一遍(或者其它什么工具)五、出core时用gdb跟进去看看反正我们调试从来不会把单步调试放到以上五条的前面的。。
登录百度帐号推荐应用
为兴趣而生,贴吧更懂你。或}

我要回帖

更多关于 c语言printf的实现 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信