2012年6月3日
对文件进行读写是常碰到操作,文件在进行读写操作之前要先打开,使用完毕要关闭。所谓打开文件,实际上是建立文件的各种有关信息,并使文件指针指向该文件,以便进行其它操作。通过c语言基础培训可以基本掌握文件进行读写操作。
文件的打开(fopen函数)
fopen函数用来打开一个文件,其调用的一般形式为:文件指针名=fopen(文件名,使用文件方式); 其中,"文件指针名"必须是被说明为FILE 类型的指针变量;"文件名"是被打开文件的文件名;"使用文件方式"是指文件的类型和操作要求。 "文件名"是字符串常量或字符串数组。
相关函数 :open,fclose
表头文件 :#include<stdio.h>
定义函数 :FILE * fopen(const char * path,const char * mode);
函数说明
参数path字符串包含欲打开的文件路径及文件名,参数mode字符串则代表着流形态。
mode有下列几种形态字符串:
r 打开只读文件,该文件必须存在。
r+ 打开可读写的文件,该文件必须存在。
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。
a+ 以附加方式打开可读写的文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾后,即文件原先的内容会被保留。
上述的形态字符串都可以再加一个b字符,如rb、w+b或ab+等组合,加入b 字符用来告诉函数库打开的文件为二进制文件,而非纯文字文件。不过在POSIX系统,包含Linux都会忽略该字符。由fopen()所建立的新文件会具有S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH(0666)权限,此文件权限也会参考umask 值。
返回值
文件顺利打开后,指向该流的文件指针就会被返回。若果文件打开失败则返回NULL,并把错误代码存在errno 中。
附加说明
一般而言,开文件后会作一些文件读取或写入的动作,若开文件失败,接下来的读写动作也无法顺利进行,所以在fopen()后请作错误判断及处理。
范例
#include<stdio.h>
main()
{
FILE * fp;
fp=fopen("noexist","a+");
if(fp= =NULL) return;
fclose(fp);
}
2012年5月26日
一般程序如果要进行优化,通常情况下是指优化程序代码或程序执行速度。优化代码和优化速度实际上是一个予盾的统一,一般是优化了代码的尺寸,就会带来执行时间的增加,如果优化了程序的执行速度,通常会带来代码增加的副作用,很难鱼与熊掌兼得,只能在设计时掌握一个平衡点。
一、程序结构的优化
1、表达式
对于一个表达式中各种运算执行的优先顺序不太明确或容易混淆的地方,应当采用圆括号明确指定它们的优先顺序。一个表达式通常不能写得太复杂,如果表达式太复杂,时间久了以后,自己也不容易看得懂,不利于以后的维护。
2、程序的书写结构
虽然书写格式并不会影响生成的代码质量,但是在实际编写程序时还是应该尊循一定的书写规则,一个书写清晰、明了的程序,有利于以后的维护。在书写程序时,特别是对于While、for、do…while、if…elst、switch…case等语句或这些语句嵌套组合时,应采用"缩格"的书写形式,
3、减少判断语句
能够使用条件编译(ifdef)的地方就使用条件编译而不使用if语句,有利于减少编译生成的代码的长度,能够不用判断语句则少用判断用语句。
4、标识符
程序中使用的用户标识符除要遵循标识符的命名规则以外,一般不要用代数符号(如a、b、x1、y1)作为变量名,应选取具有相关含义的英文单词(或缩写)或汉语拼音作为标识符,以增加程序的可读性,如:count、number1、red、work等。
5、定义常数
在程序化设计过程中,对于经常使用的一些常数,如果将它直接写到程序中去,一旦常数的数值发生变化,就必须逐个找出程序中所有的常数,并逐一进行修改,这样必然会降低程序的可维护性。因此,应尽量当采用预处理命令方式来定义常数,而且还可以避免输入错误。
二、代码的优化
1、使用自加、自减指令
通常使用自加、自减指令和复合赋值表达式(如a-=1及a+=1等)都能够生成高质量的程序代码,编译器通常都能够生成inc和dec之类的指令,而使用a=a+1或a=a-1之类的指令,有很多C编译器都会生成二到三个字节的指令。在AVR单片适用的ICCAVR、GCCAVR、IAR等C编译器以上几种书写方式生成的代码是一样的,也能够生成高质量的inc和dec之类的的代码。
2、查表
在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学模型的插补运算,对这些即消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数据表置于程序存储区。如果直接生成所需的表比较困难,也尽量在启动时先计算,然后在数据存储器中生成所需的表,后以在程序运行直接查表就可以了,减少了程序执行过程中重复计算的工作量。
3、使用尽量小的数据类型
能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。在ICCAVR中,可以在Options中设定使用printf参数,尽量使用基本型参数(%c、%d、%x、%X、%u和%s格式说明符),少用长整型参数(%ld、%lu、%lx和%lX格式说明符),至于浮点型的参数(%f)则尽量不要使用,其它C编译器也一样。在其它条件不变的情况下,使用%f参数,会使生成的代码的数量增加很多,执行速度降低。
4、选择合适的算法和数据结构
应该熟悉算法语言,知道各种算法的优缺点,具体资料请参见相应的参考资料,有很多计算机书籍上都有介绍。将比较慢的顺序查找法用较快的二分查找或乱序查找法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,都可以大大提高程序执行的效率选择一种合适的数据结构也很重要,比如你在一堆随机存放的数中使用了大量的插入和删除指令,那使用链表要快得多。数组与指针语句具有十分密码的关系,一般来说,指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。但是在Keil中则相反,使用数组比使用的指针生成的代码更短。
2012年4月8日
#include<stdlib.h>
#include<stdio.h>
void swap1(int x,int y)
{
int temp;
temp=x;
x=y;
y=temp;
}
void swap2(int *x,int *y)
{
int *temp;
temp=x;
x=y;
y=temp;
}
void swap3(int *x,int *y)
{
int temp;
temp=*x;
*x=*y;
*y=temp;
}
void swap4(int a[],int b[])
{
int temp;
temp=a[0];
a[0]=b[0];
b[0]=temp;
}
void swap5(int a[],int b[])
{
int temp;
temp=*a;
*a=*b;
*b=temp;
}
int main()
{
int x,y;
x=4;
y=3;
swap1(x,y);
printf("swap1: x:%d,y:%d\n",x,y);//形参传值,不能交换,实际传过去是拷贝的一份,没改变主函数中x,y
swap2(&x,&y);
printf("swap2: x:%d,y:%d\n",x,y);//不能交换,函数中只是地址交换了下,地址指向的内容没有交换
swap3(&x,&y);
printf("swap3: x:%d,y:%d\n",x,y);//能交换,地址指向的内容进行了交换
swap4(&x,&y);
printf("swap4: x:%d,y:%d\n",x,y);//能交换,地址指向的内容进行交换
swap5(&x,&y);
printf("swap5: x:%d,y:%d\n",x,y);//能交换,地址指向的内容进行交换
return 0;
}
swap1: x:4,y:3
swap2: x:4,y:3
swap3: x:3,y:4
swap4: x:4,y:3
swap5: x:3,y:4
2012年3月28日
本文提供了一个用于对 C/
C++ 程序进行编译和连接以产生可执行程序的通用 Makefile.
在使用 Makefile 之前,只需对它进行一些简单的设置即可;而且一经设置,即使以后对源程序文件有所增减一般也不再需要改动 Makefile.因此,即便是一个没有学习过 Makefile 书写规则的人,也可以为自己的 C/C++ 程序快速建立一个可工作的 Makefile.
这个 Makefile 可以在 GNU Make 和 GCC 编译器下正常工作。但是不能保证对于其它版本的 Make 和编译器也能正常工作。
如果你发现了本文中的错误,或者对本文有什么感想或建议,可通过 whyglinux AT hotmail DOT com 邮箱和作者联系。
此 Makefile 的使用方法如下:[list=1][*]程序目录的组织尽量将自己的源程序集中在一个目录中,并且把 Makefile 和源程序放在一起,这样用起来比较方便。当然,也可以将源程序分类存放在不同的目录中。
在程序目录中创建一个名为 Makefile 的文本文件,将后面列出的 Makefile 的内容复制到这个文件中。(注意:在复制的过程中,Makfile 中各命令前面的 Tab 字符有可能被转换成若干个空格。这种情况下需要把 Makefile 命令前面的这些空格替换为一个 Tab.)
将当前工作目录切换到 Makefile 所在的目录。目前,这个 Makefile 只支持在当前目录中的调用,不支持当前目录和 Makefile 所在的路径不是同一目录的情况。
[*]指定可执行文件程序编译和连接成功后产生的可执行文件在 Makefile 中的 PROGRAM 变量中设定。这一项不能为空。为自己程序的可执行文件起一个有意义的名子吧。
[*]指定源程序要编译的源程序由其所在的路径和文件的扩展名两项来确定。由于头文件是通过包含来使用的,所以在这里说的源程序不应包含头文件。
程序所在的路径在 SRCDIRS 中设定。如果源程序分布在不同的目录中,那么需要在 SRCDIRS 中一一指定,并且路径名之间用空格分隔。
在 SRCEXTS 中指定程序中使用的文件类型。C/C++ 程序的扩展名一般有比较固定的几种形式:。c、。C、。cc、。cpp、。CPP、。c++、。cp、或者。cxx(参见 man gcc)。扩展名决定了程序是 C 还是 C++ 程序:。c 是 C 程序,其它扩展名表示 C++ 程序。一般固定使用其中的一种扩展名即可。但是也有可能需要使用多种扩展名,这可以在 SOURCE_EXT 中一一指定,各个扩展名之间用空格分隔。
虽然并不常用,但是 C 程序也可以被作为 C++ 程序编译。这可以通过在 Makefile 中设置 CC = $(CXX) 和 CFLAGS = $(CXXFLAGS) 两项即可实现。
这个 Makefile 支持 C、C++ 以及 C/C++ 混合三种编译方式:[list][*]如果只指定 .c 扩展名,那么这是一个 C 程序,用 $(CC) 表示的编译命令进行编译和连接。
[*]如果指定的是除 .c 之外的其它扩展名(如 .cc、。cpp、。cxx 等),那么这是一个 C++ 程序,用 $(CXX) 进行编译和连接。
[*]如果既指定了 .c,又指定了其它 C++ 扩展名,那么这是 C/C++ 混合程序,将用 $(CC) 编译其中的 C 程序,用 $(CXX) 编译其中的 C++ 程序,最后再用 $(CXX) 连接程序。
[/list]这些工作都是 make 根据在 Makefile 中提供的程序文件类型(扩展名)自动判断进行的,不需要用户干预。
[*]指定编译选项编译选项由三部分组成:预处理选项、编译选项以及连接选项,分别由 CPPFLAGS、CFLAGS与CXXFLAGS、LDFLAGS 指定。
CPPFLAGS 选项可参考 C 预处理命令 cpp 的说明,但是注意不能包含 -M 以及和 -M 有关的选项。如果是 C/C++ 混合编程,也可以在这里设置 C/C++ 的一些共同的编译选项。
CFLAGS 和 CXXFLAGS 两个变量通常用来指定编译选项。前者仅仅用于指定 C 程序的编译选项,后者仅仅用于指定 C++ 程序的编译选项。其实也可以在两个变量中指定一些预处理选项(即一些本来应该放在 CPPFLAGS 中的选项),和 CPPFLAGS 并没有明确的界限。
连接选项在 LDFLAGS 中指定。如果只使用 C/C++ 标准库,一般没有必要设置。如果使用了非标准库,应该在这里指定连接需要的选项,如库所在的路径、库名以及其它联接选项。
现在的库一般都提供了一个相应的 .pc 文件来记录使用库所需要的预编译选项、编译选项和连接选项等信息,通过 pkg-config 可以动态提取这些选项。与由用户显式指定各个选项相比,使用 pkg-config 来访问库提供的选项更方便、更具通用性。在后面可以看到一个 GTK+ 程序的例子,其编译和连接选项的指定就是用 pkg-config 实现的。
[*]编译和连接上面的各项设置好之后保存 Makefile 文件。执行 make 命令,程序就开始编译了。
命令 make 会根据 Makefile 中设置好的路径和文件类型搜索源程序文件,然后根据文件的类型调用相应的编译命令、使用相应的编译选项对程序进行编译。
编译成功之后程序的连接会自动进行。如果没有错误的话最终会产生程序的可执行文件。
注意:在对程序编译之后,会产生和源程序文件一一对应的 .d 文件。这是表示依赖关系的文件,通过它们 make 决定在源程序文件变动之后要进行哪些更新。为每一个源程序文件建立相应的 .d 文件这也是 GNU Make 推荐的方式。
[*]Makefile 目标(Targets)
下面是关于这个 Makefile 提供的目标以及它所完成的功能:[list][*]make编译和连接程序。相当于 make all. [*]make objs仅仅编译程序产生 .o 目标文件,不进行连接(一般很少单独使用)。
[*]make clean删除编译产生的目标文件和依赖文件。
[*]make cleanall删除目标文件、依赖文件以及可执行文件。
[*]make rebuild重新编译和连接程序。相当于 make clean && make all. [/list][/list]关于这个 Makefile 的实现原理不准备详细解释了。如果有兴趣的话,可参考文末列出的“参考资料”。
Makefile 的内容如下:############################################################################### # # Generic Makefile for C/C++ Program # # Author: whyglinux (whyglinux AT hotmail DOT com) # Date: 2006/03/04 # Description: # The makefile searches in <SRCDIRS> directories for the source files # with extensions specified in <SOURCE_EXT>, then compiles the sources # and finally produces the <PROGRAM>, the executable file, by linking # the objectives. # Usage: # $ make compile and link the program. # $ make objs compile only (no linking. Rarely used)。 # $ make clean clean the objectives and dependencies. # $ make cleanall clean the objectives, dependencies and executable. # $ make rebuild rebuild the program. The same as make clean && make all. #============================================================================== ## Customizing Section: adjust the following if necessary. ##============================================================================= # The executable file name. # It must be specified. # PROGRAM := a.out # the executable name PROGRAM := # The directories in which source files reside. # At least one path should be specified. # SRCDIRS := . # current directory SRCDIRS := # The source file types (headers excluded)。 # At least one type should be specified. # The valid suffixes are among of .c, .C, .cc, .cpp, .CPP, .c++, .cp, or .cxx. # SRCEXTS := .c # C program # SRCEXTS := .cpp # C++ program # SRCEXTS := .c .cpp # C/C++ program SRCEXTS := # The flags used by the cpp (man cpp for more)。 # CPPFLAGS := -Wall -Werror # show all warnings and take them as errors CPPFLAGS := # The compiling flags used only for C. # If it is a C++ program, no need to set these flags. # If it is a C and C++ merging program, set these flags for the C parts. CFLAGS := CFLAGS += # The compiling flags used only for C++. # If it is a C program, no need to set these flags. # If it is a C and C++ merging program, set these flags for the C++ parts. CXXFLAGS := CXXFLAGS += # The library and the link options ( C and C++ common)。 LDFLAGS := LDFLAGS += ## Implict Section: change the following only when necessary. ##============================================================================= # The C program compiler. Uncomment it to specify yours explicitly. #CC = gcc # The C++ program compiler. Uncomment it to specify yours explicitly. #CXX = g++ # Uncomment the 2 lines to compile C programs as C++ ones. #CC = $(CXX) #CFLAGS = $(CXXFLAGS) # The command used to delete file. #RM = rm -f ## Stable Section: usually no need to be changed. But you can add more. ##============================================================================= SHELL = /bin/sh SOURCES = $(foreach d,$(SRCDIRS),$(wildcard $(addprefix $(d)/*,$(SRCEXTS)))) OBJS = $(foreach x,$(SRCEXTS), \ $(patsubst %$(x),%.o,$(filter %$(x),$(SOURCES)))) DEPS = $(patsubst %.o,%.d,$(OBJS)) .PHONY : all objs clean cleanall rebuild all : $(PROGRAM) # Rules for creating the dependency files (。d)。 #—— %.d : %.c @$(CC) -MM -MD $(CFLAGS) $< %.d : %.C @$(CC) -MM -MD $(CXXFLAGS) $< %.d : %.cc @$(CC) -MM -MD $(CXXFLAGS) $< %.d : %.cpp @$(CC) -MM -MD $(CXXFLAGS) $< %.d : %.CPP @$(CC) -MM -MD $(CXXFLAGS) $< %.d : %.c++ @$(CC) -MM -MD $(CXXFLAGS) $< %.d : %.cp @$(CC) -MM -MD $(CXXFLAGS) $< %.d : %.cxx @$(CC) -MM -MD $(CXXFLAGS) $< # Rules for producing the objects. #—— objs : $(OBJS) %.o : %.c $(CC) -c $(CPPFLAGS) $(CFLAGS) $< %.o : %.C $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $< %.o : %.cc $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $< %.o : %.cpp $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $< %.o : %.CPP $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $< %.o : %.c++ $(CXX -c $(CPPFLAGS) $(CXXFLAGS) $< %.o : %.cp $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $< %.o : %.cxx $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $< # Rules for producing the executable. #—— $(PROGRAM) : $(OBJS) ifeq ($(strip $(SRCEXTS)), .c) # C file $(CC) -o $(PROGRAM) $(OBJS) $(LDFLAGS) else # C++ file $(CXX) -o $(PROGRAM) $(OBJS) $(LDFLAGS) endif -include $(DEPS) rebuild: clean all clean : @$(RM) *.o *.d cleanall: clean @$(RM) $(PROGRAM) $(PROGRAM)。exe ### End of the Makefile ## Suggestions are welcome ## All rights reserved ### ###############################################################################
下面提供两个例子来具体说明上面 Makefile 的用法。
[color=darkred]例一 Hello World 程序[/color]
这个程序的功能是输出 Hello, world! 这样一行文字。由 hello.h、hello.c、main.cxx 三个文件组成。前两个文件是 C 程序,后一个是 C++ 程序,因此这是一个 C 和 C++ 混编程序。
/* File name: hello.h * C header file */ #ifndef HELLO_H #define HELLO_H #ifdef __cplusplus extern "C" { #endif void print_hello(); #ifdef __cplusplus } #endif #endif
/* File name: hello.c * C source file. */ #include "hello.h" #include <stdio.h> void print_hello() { puts( "Hello, world!" ); }
/* File name: main.cxx * C++ source file. */ #include "hello.h" int main() { print_hello(); return 0; }
建立一个新的目录,然后把这三个文件拷贝到目录中,也把 Makefile 文件拷贝到目录中。之后,对 Makefile 的相关项目进行如下设置:PROGRAM := hello # 设置运行程序名 SRCDIRS := . # 源程序位于当前目录下 SRCEXTS := .c .cxx # 源程序文件有 .c 和 .cxx 两种类型 CFLAGS := -g # 为 C 目标程序包含 GDB 可用的调试信息 CXXFLAGS := -g # 为 C++ 目标程序包含 GDB 可用的调试信息
由于这个简单的程序只使用了 C 标准库的函数(puts),所以对于 CFLAGS 和 CXXFLAGS 没有过多的要求,LDFLAGS 和 CPPFLAGS 选项也无需设置。
经过上面的设置之后,执行 make 命令就可以编译程序了。如果没有错误出现的话,。/hello 就可以运行程序了。
如果修改了源程序的话,可以看到只有和修改有关的源文件被编译。也可以再为程序添加新的源文件,只要它们的扩展名是已经在 Makefile 中设置过的,那么就没有必要修改 Makefile.
[color=darkred]例二 GTK+ 版 Hello World 程序[/color]
这个 GTK+ 2.0 版的 Hello World 程序可以从下面的网址上得到:http://www.gtk.org/tutorial/c58.html#SEC-HELLOWORLD.当然,要编译 GTK+ 程序,还需要你的系统上已经安装好了 GTK+.
跟第一个例子一样,单独创建一个新的目录,把上面网页中提供的程序保存为 main.c 文件。对 Makefile 做如下设置:PROGRAM := hello # 设置运行程序名 SRCDIRS := . # 源程序位于当前目录下 SRCEXTS := .c # 源程序文件只有 .c 一种类型 CFLAGS := `pkg-config ——cflags gtk+-2.0` # CFLAGS LDFLAGS := `pkg-config ——libs gtk+-2.0` # LDFLAGS
这是一个 C 程序,所以 CXXFLAGS 没有必要设置——即使被设置了也不会被使用。
编译和连接 GTK+ 库所需要的 CFLAGS 和 LDFLAGS 由 pkg-config 程序自动产生。
现在就可以运行 make 命令编译、。/hello 执行这个 GTK+ 程序了。
2012年3月20日
多继承可以看作是单继承的扩展。所谓多继承是指派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。
多继承下派生类的定义格式如下:
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
其中,<继承方式1>,<继承方式2>,…是三种继承方式:public、private、protected之一。例如:
class A
{
…
};
class B
{
…
};
class C : public A, public B
{
…
};
其中,派生类C具有两个基类(类A和类B),因此,类C是多继承的。按照继承的规定,派生类C的成员包含了基类A, B中成员以及该类本身的成员。
多继承的构造函数
在多继承的情况下,派生类的构造函数格式如下:
<派生类名>(<总参数表>):<基类名1>(<参数表1>),<基类名2>(<参数表2>),…
<子对象名>(<参数表n+1>),…
{
<派生类构造函数体>
}
其中,<总参数表>中各个参数包含了其后的各个分参数表。
多继承下派生类的构造函数与单继承下派生类构造函数相似,它必须同时负责该派生类所有基类构造函数的调用。同时,派生类的参数个数必须包含完成所有基类初始化所需的参数个数。
派生类构造函数执行顺序是先执行所属基类的构造函数,再执行派生类本身构造函数,处于同一层次的各基类构造函数的执行顺序取决于定义派生类时所指定的各基类顺序,与派生类构造函数中所定义的成员初始化列表的各项顺序无关。也就是说,执行基类构造函数的顺序取决于定义派生类时基类的顺序。可见,派生类构造函数的成员初始化列表中各项顺序可以任意地排列。
下面通过一个例子来说明派生类构造函数的构成及其执行顺序。
#include <iostream.h>
class B1
{
public:
B1(int i)
{
b1 = i;
cout《"构造函数 B1."《i《 endl;
}
void print()
{
cout《"B1.print()"《b1《endl;
}
private:
int b1;
};
class B2
{
public:
B2(int i)
{
b2 = i;
cout《"构造函数 B2."《i《 endl;
}
void print()
{
cout《"B2.print()"《b2《endl;
}
private:
int b2;
};
class B3
{
public:
B3(int i)
{
b3 = i;
cout《"构造函数 B3."《i《endl;
}
int getb3()
{
return b3;
}
private:
int b3;
};
class A : public B2, public B1
{
public:
A(int i, int j, int k, int l):B1(i), B2(j), bb(k)
{
a = l;
cout《"构造函数 A."《a《endl;
}
void print()
{
B1::print();
B2::print();
cout《"A.print()"《a《","《bb.getb3()《endl;
}
private:
int a;
B3 bb;
};
void main()
{
A aa(1, 2, 3, 4);
aa.print();
}
该程序的输出结果为:
构造函数 B2.2
构造函数 B1.1
构造函数 B3.3
构造函数 A.4
B1.print()。1
B2.print()2
A.print()4, 3
在该程序中,作用域运算符::用于解决作用域冲突的问题。在派生类A中的print()函数的定义中,使用了B1::print;和B2::print();语句分别指明调用哪一个类中的print()函数,这种用法应该学会。
二义性问题
一般说来,在派生类中对基类成员的访问应该是唯一的,但是,由于多继承情况下,可能造成对基类中某成员的访问出现了不唯一的情况,则称为对基类成员访问的二义性问题。
实际上,在上例已经出现过这一问题,回忆一下上例中,派生类A的两基类B1和B2中都有一个成员函数print()。如果在派生类中访问 print()函数,到底是哪一个基类的呢?于是出现了二义性。但是在上例中解决了这个问题,其办法是通过作用域运算符::进行了限定。如果不加以限定,则会出现二义性问题。
下面再举一个简单的例子,对二义性问题进行深入讨论。例如:
class A
{
public:
void f();
};
class B
{
public:
void f();
void g();
};
class C : public A, public B
{
public:
void g();
void h();
};
如果定义一个类C的对象c1:
C c1;
则对函数f()的访问
c1.f();
便具有二义性:是访问类A中的f(),还是访问类B中的f()呢?
解决的方法可用前面用过的成员名限定法来消除二义性,例如:
c1.A::f();
或者
c1.B::f();
但是,最好的解决办法是在类C中定义一个同名成员f(),类C中的f()再根据需要来决定调用A::f(),还是B::f(),还是两者皆有,这样,c1.f()将调用C::f()。
同样地,类C中成员函数调用f()也会出现二义性问题。例如:
viod C::h()
{
f();
}
这里有二义性问题,该函数应修改为:
void C::h()
{
A::f();
}
或者
void C::h()
{
B::f();
}
或者
void C::f()
{
A::f();
B::f();
}
另外,在前例中,类B中有一个成员函数g(),类C中也有一个成员函数g()。这时,
c1.g();
不存在二义性,它是指C::g(),而不是指B::g()。因为这两个g()函数,一个出现在基类B,一个出现在派生类C,规定派生类的成员将支配基类中的同名成员。因此,上例中类C中的g()支配类B中的g(),不存在二义性,可选择支配者的那个名字。
当一个派生类从多个基类派生类,而这些基类又有一个共同的基类,则对该基类中说明的成员进行访问时,也可能会出现二义性。例如:
class A
{
public:
int a;
};
class B1 : public A
{
private:
int b1;
};
class B2 : public A
{
private:
int b2;
};
class C : public B1, public B2
{
public:
int f();
private:
int c;
};
已知:C c1;
下面的两个访问都有二义性:
c1.a;
c1.A::a;
而下面的两个访问是正确的:
c1.B1::a;
c1.B2::a;
类C的成员函数f()用如下定义可以消除二义性:
int C::f()
{
retrun B1::a + B2::a;
}
由于二义性的原因,一个类不可以从同一个类中直接继承一次以上,例如:
class A : public B, public B
{
…
}
这是错误的。
2012年3月18日
呵呵,最近几天我有个小发现,那就是老白没有来看过我的博客了,说真的蛮希望他能来的,
他不来有点让我失望,毕竟我也关注他很长一段时间了,当然,不管他来不来,我自己的工作还是得继续下去的嘛中,对不对,这里我将简单对于将n个实数由大到小排序做个介绍吧。
n个实数用数组a描述。
本例提供用选择排序方法与冒泡排序方法分别实现n个实数由大到小排序的函数。
算法一:选择排序。
选择排序需反复进行求最大值与交换两个数这两种基本操作。
对a[o]、a[1]、…、a[n一1]由大到小排序:先求所有数的最大值,然后将最大值与a[o]进行交换;再求a[1]~a[n一1]这些数的最大值,然后将最大值与a[1]进行交换;再求a[2]~a[n一1]这些数的最大值,然后将最大值与a[2]进行交换……;最后求a[n一2]与a[n一1]这些数的最大值,然后将最大值与a[n一2]进行交换。如此,经过n一1轮处理完成排序,本文首发中国自学编程网。
程序如下:
void sortl(a,n)/*选择排序函数*/
float a[];
int n:
{int k,i,j;/*k最大值下标,i,j循环控制变量*/
float t;/*中间变量,用于两个数的交换*/
for(i=0;i<n-1;i++)
{k=i;/*求最大值下标*/
for(j=i+1}j<n;j++)
if(a[j]>a[k])k=j
t=a[i];a[i]一a[k];a[k]=t;/*进行交换*/
}
}
算法二:冒泡排序。
冒泡排序需反复进行相邻两个数的比较与交换两个数这两种基本操作。对相邻的两个数进行比较时,如果后面的数大于前面的数,将这两个数进行交换,大的数往前冒。将所有相邻的两个安全阀数比较一遍,称为一轮比较。如果进行一轮比较无交换,本文首发中国自学编程网排序完成。
有无交换用一标志变量描述,一轮比较用for循环完成,整个排序利用标志变量用条件循环控制。
程序如下:
void sort2(a,n)/*冒泡排序函数*/
float a[];
int n;
{int i;/*一轮比较的循环控制变量*/
int flag;/*标志变量,为1有交换,为0无交换*/
float t;/*中间变量,用于两个数的交换*/
do
{flag=O;/*先假定无交换,已排好序*/
for(i=O;i<n一2; i++)
if(a[i+1]>a[i])
{t=a[i];a[i]=a[i+1];a[i+1]=t;/*进行交换*/
flag=1;/*有交换,标志变量的值改变为1*/
}
}while(flag==1);
)
由小到大排序请读者作类似考虑。呵呵,差不多了,如果有不当之处,请朋友们指正啊---
2012年3月11日
位段以位为单位定义结构体(或共用体)中成员所占存储空间的长度。
含有位段的结构体类型称为位段结构。
位段结构也是一种结构体类型,只不过其中含有以位为单位定义存储长度的整数类型位段成员。采用位段结构既节省存储空间,又可方便操作。
位段结构中位段的定义格式为:
unsigned <成员名>:<二进制位数>
例如:
struct bytedata
{unsigned a:2; /*位段a,占2位*/
unsigned:6; /*无名位段,占6位,但不能访问*/
unsigned:0; /*无名位段,占0位,表下一位段从下一字边界开始*/
unsigned b:10; /*位段b,占10位*/
int i; /*成员i,从下一字边界开始*/
}data;
位段数据的引用:
同结构体成员中的数据引用一样,但应注意位段的最大取值范围不要超出二进制位数定的范围,否则超出部分会丢弃。
例如:data.a=2; 但 data.a=10;就超出范围(a占2位,最大3)
关于位段数据,注意以下几点:
(1)一个位段必须存储在同一存储单元(即字)之中,不能跨两个单元。如果其单元空间不够,则剩余空间不用,从下一个单元起存放该位段。
(2)可以通过定义长度为0的位段的方式使下一位段从下一存储单元开始。
(3)可以定义无名位段。
(4)位段的长度不能大于存储单元的长度。
(5)位段无地址,不能对位段进行取地址运算。
(6)位段可以以%d,%o,%x格式输出。
(7)位段若出现在表达式中,将被系统自动转换成整数。
-------------------------------------------------------
C语言中用结构实现位段--个人心血!值得一看哦!C语言中的结构是有实现位段的能力的,噢!你问它到底是什么形式是吧?这个问题呆会给你答案。让我们先看看位段的作用:位段是在字段的声明后面加一个冒号以及一个表示字段位长的整数来实现的。这种用法又被就叫作“深入逻辑元件的编程”,如果你对系统编程感兴趣,那么这篇文章你就不应该错过!
我把使用位段的几个理由告诉大家:1、它能把长度为奇数的数据包装在一起,从而节省存储的空间;2、它可以很方便地访问一个整型值的部分内容。
首先我要提醒大家注意几点:1、位段成员只有三种类型:int ,unsigned int 和signed int这三种(当然了,int型位段是不是可以取负数不是我说了算的,因为这是和你的编译器来决定的。位段,位段,它是用来表示字段位长(bit)的,它只有整型值,不会有7.2这种float类型的,如果你说有,那你就等于承认了有7.2个人这个概念,当然也没有char这个类型的);2、成员名后面的一个冒号和一个整数,这个整数指定该位段的位长(bit);3、许多编译器把位段成员的字长限制在一个int的长度范围之内;4、位段成员在内存的实现是从左到右还是从右到左是由编译器来决定的,但二者皆对。
下面我们就来看看,它到底是什么东西(我先假定大家的机器字长为32位):
Struct WORD
{
unsigned int chara: 6:
unsigned int font : 7;
unsigned int maxsize : 19;
};
Struct WORD chone;
这一段是从我编写的一个文字格式化软件摘下来的,它最多可以容纳64(既我说的unsigned int chara :6; 它总共是6位)个不同的字符值,可以处理128(既unsigned int font : 7 ;既2的7次方)种不同的字体,和2的19次方的单位长度的字。大家都可以看到maxsize是19位,它是无法被一个short int 类型的值所容纳的,我们又可以看到其余的成员的长度比char还小,这就让我们想起让他们共享32位机器字长,这就避免用一个32位的整数来表示maxsize的位段。怎么样?还要注意的是刚才的那一段代码在16位字长的机器上是无法实现的,为什么?提醒你一下,看看上面提醒的第3点,你会明白的!
你是不是发现这个东西没有用啊?如果你点头了,那你就错了!这么伟大的创造怎么会没有用呢(你对系统编程不感兴趣,相信你会改变这么一个观点的)?磁盘控制器大家应该知道吧?软驱与它的通信我们来看看是怎么实现的下面是一个磁盘控制器的寄存器:
│←5→│←5→│←9→│←8→│←1→│←1→∣←1→∣←1→∣←1→∣
上面位段从左到右依次代表的含义为:5位的命令,5位的扇区,9位的磁道,8位的错误代码,1位的HEAD LOADED,1位的写保护,1位的DISK SPINNING,1位的错误判断符,还有1位的READY位。它要怎么来实现呢?你先自己写写看:
struct DISK_FORMAT
{
unsigned int command : 5;
unsigned sector : 5;
unsigned track : 9 ;
unsigned err_code : 8;
unsigned ishead_loaded : 1;
unsigned iswrit_protect : 1;
unsigned isdisk_spinning : 1;
unsigned iserr_ocur : 1;
undigned isready :1 ;
};
注:代码中除了第一行使用了unsigned int 来声明位段后就省去了int ,这是可行的,详见ANCI C标准。
如果我们要对044c18bfH的地址进行访问的话,那就这样:
#define DISK ((struct DISK_FORMAT *)0x044c18bf)
DISK->sector=fst_sector;
DISK->track=fst_track;
DISK->command=WRITE;
当然那些都是要宏定义的哦!
我们用位段来实现这一目的是很方便的,其实这也可以用移位或屏蔽来实现,你尝试过就知道哪个更方便了!
2012年2月26日
使用expat的原因很多,主要还是因为expat更灵活。习惯了TinyXML,一开始不太习惯expat,分析一下,其实很容易上手的。
1.回调函数
以下案例解析xml文件中的elment,attribute和text。expat使用回调方式返回xml数据,解析器解析到一个element及其内部属性后,将调用事先设置好的函数,同样,当element结束和text结束后,也会分别调用对应的函数。
2.如何处理数据之间的包含关系
典型的方式是定义三个函数分别处理elment开始(含属性)、element结束和文本内容。回调函数的第一个参数是自定义的,通常用于存储 XML文档的上下文信息,用XML_SetUserData可以设置这个参数,下例中传递一个整数指针,以便在每次回调时能知道该元素是第几层元素。
该参数也可以是一个栈对象的地址,开始一个元素时,将新元素对应的数据压入堆栈,处理下一级元素时,新元素是栈顶元素在子元素,然后处理完了继续把该元素压入堆栈,继续下一级新的子元素。当元素结束后,需要出栈,以便解析下个兄弟元素程时能取到父节点。
好啦,基本应用还是很简单的,实际上Expat的API函数不多。
3.如何处理属性
属性通过ElementHandler回调函数传入,这里有一个char** atts就是属性,这是一个字符指针数组,如果有N个属性,数组大小就是2*N+1,最后一个素组元素为空指针,奇数指针对应属性名称,偶数指针对应属性值(字符串格式)。可以在一个循环中处理多个属性,当遇到空指针时,表示没有更多属性了。
好啦,先看sample吧:
#include <stdio.h>
#include "expat.h"
#pragma warning(disable:4996)
#define XML_FMT_INT_MOD "l"
static void XMLCALL startElement(void *userData, const char *name, const char **atts)
{
int i;
int *depthPtr = (int *)userData;
for (i = 0; i < *depthPtr; i++)
printf(" ");
printf(name);
*depthPtr += 1;
for(i=0;atts[i]!=0;i+=2)
{
printf(" %s=%s",atts[i],atts[i+1]);
}
printf("\n");
}
static void XMLCALL endElement(void *userData, const char *name)
{
int *depthPtr = (int *)userData;
*depthPtr -= 1;
}
int main(int argc, char *argv[])
{
char buf[BUFSIZ]; XML_Parser parser = XML_ParserCreate(NULL);
int done; int depth = 0;
XML_SetUserData(parser, &depth);
XML_SetElementHandler(parser, startElement, endElement);
FILE* pFile= argc<2 ?stdin : fopen(argv[1],"rb");
do
{ int len = (int)fread(buf, 1, sizeof(buf), pFile);
done = len < sizeof(buf);
if (XML_Parse(parser, buf, len, done) == XML_STATUS_ERROR)
{
fprintf(stderr,"%s at line %" XML_FMT_INT_MOD "u\n",
XML_ErrorString(XML_GetErrorCode(parser)),
XML_GetCurrentLineNumber(parser));
return 1;
}
}
while (!done);
XML_ParserFree(parser);
fclose(pFile);
return 0;
}
4.其他ElementHanlder
expat还可以设置CData,Comment的handler,另外一些函数本人还没使用过,涉及到更多的xml标准的知识,如果需要,可以参考官方的手册。
2012年2月22日
一、摘要
JSON 的全称为:JavaScript Object Notation,顾名思义,JSON 是用于标记 Javascript 对象的,JSON 官方的解释为:JSON 是一种轻量级的数据传输格式。
本文并不详细介绍 JSON 本身的细节,旨在讨论如何使用 C++ 语言来处理 JSON。关于 JSON 更具体的信息,可参见 JSON 官网:http://www.json.org。
二、本文选择处理 JSON的 C++ 库
本文选择一个第三方库 jsoncpp 来解析 JSON。jsoncpp 是比较出名的 C++ JSON 解析库。在 JSON 官网也是首推的。
下载地址为:http://sourceforge.net/projects/jsoncpp。本文使用的 jsoncpp 版本为:0.5.0。
三、jsoncpp 在 Windows 下的编译
要使用第三方源码库,第一步少不了的就是编译,将源码文件编译成我们方便使用的动态链接库、静态链接库或者静态导入库[1]。
jsconcpp 进行 JSON 解析的源码文件分布在 include/json、src/lib_json 下。其实 jsoncpp 源码并不多,为了方便产品管理,此处没必要将其编译为动态链接库或者静态导入库,所以我们选择使用静态链接库[2]。
jsoncpp 已经处理的很完善了,所有编译选项都已经配置好,打开makefiles/vs71/jsoncpp.sln 便可以开始编译(默认是使用 VS2003 编译器的,打开时直接按照 VS2005 提示转换即可)。
四、jsoncpp 使用详解
jsoncpp 主要包含三种类型的 class:Value、Reader、Writer。jsoncpp 中所有对象、类名都在 namespace Json 中,包含 json.h 即可。
Json::Value 只能处理 ANSI 类型的字符串,如果 C++ 程序是用 Unicode 编码的,最好加一个 Adapt 类来适配。
1、Value
Json::Value 是jsoncpp 中最基本、最重要的类,用于表示各种类型的对象,jsoncpp 支持的对象类型可见 Json::ValueType 枚举值。
可如下是用 Json::Value 类:
Json::Value json_temp; // 临时对象,供如下代码使用
json_temp["name"] = Json::Value("huchao");
json_temp["age"] = Json::Value(26);
Json::Value root; // 表示整个 json 对象
root["key_string"] = Json::Value("value_string"); // 新建一个 Key(名为:key_string),赋予字符串值:"value_string"。
root["key_number"] = Json::Value(12345); // 新建一个 Key(名为:key_number),赋予数值:12345。
root["key_boolean"] = Json::Value(false); // 新建一个 Key(名为:key_boolean),赋予bool值:false。
root["key_double"] = Json::Value(12.345); // 新建一个 Key(名为:key_double),赋予 double 值:12.345。
root["key_object"] = Json_temp; // 新建一个 Key(名为:key_object),赋予 json::Value 对象值。
root["key_array"].append("array_string"); // 新建一个 Key(名为:key_array),类型为数组,对第一个元素赋值为字符串:"array_string"。
root["key_array"].append(1234); // 为数组 key_array 赋值,对第二个元素赋值为:1234。
Json::ValueType type = root.type(); // 获得 root 的类型,此处为 objectValue 类型。
注:跟C++ 不同,JavaScript 数组可以为任意类型的值,所以 jsoncpp 也可以。
如上几个用法已经可以满足绝大部分 json 应用了,当然 jsoncpp 还有一些其他同能,比如说设置注释、比较 json 大小、交换 json 对象等,都很容易使用,大家自己尝试吧。
2、Writer
如上说了 Json::Value 的使用方式,现在到了该查看刚才赋值内容的时候了,查看 json 内容,使用 Writer 类即可。
Jsoncpp 的 Json::Writer 类是一个纯虚类,并不能直接使用。在此我们使用 Json::Writer 的子类:Json::FastWriter、Json::StyledWriter、Json::StyledStreamWriter。
顾名思义,用 Json::FastWriter 来处理 json 应该是最快的,下面我们来试试。
Json::FastWriter fast_writer;
std::cout << fast_writer.write(root) << std::endl;
输出结果为:
{"key_array":["array_string",1234],"key_boolean":false,"key_double":12.3450,"key_number":12345,"key_object":{"age":26,"name":"huchao"},"key_string":"value_string"}
再次顾名思义,用 Json::StyledWriter 是格式化后的 json,下面我们来看看 Json::StyledWriter 是怎样格式化的。
Json::StyledWriter styled_writer;
std::cout << styled_writer.write(root) << std::endl;
输出结果为:
{
"key_array" : [ "array_string", 1234 ],
"key_boolean" : false,
"key_double" : 12.3450,
"key_number" : 12345,
"key_object" : {
"age" : 26,
"name" : "huchao"
},
"key_string" : "value_string"
}
3、Reader
Json::Reader 是用于读取的,说的确切点,是用于将字符串转换为 Json::Value 对象的,下面我们来看个简单的例子。
Json::Reader reader;
Json::Value json_object;
const char* json_document = "{\"age\" : 26,\"name\" : \"huchao\"}";
if (!reader.parse(json_document, json_object))
return 0;
std::cout << json_object["name"] << std::endl;
std::cout << json_object["age"] << std::endl;
输出结果为:
"huchao"
26
可见,上述代码已经解析出了 json 字符串。
2012年2月17日
当你涉及到C/
C++的核心编程的时候,你会无止境地与内存管理打交道。这些往往会使人受尽折磨。所以如果你想深入C/
C++编程,你必须静下心来,好好苦一番。
现在我们将讨论C/C++里我认为哪一本书都没有完全说清楚,也是涉及概念细节最多,语言中最难的技术之一的动态内存的传递。并且在软件开发中很多专业人员并不能写出相关的合格的代码。
一、引入
看下面的例子,这是我们在编写库函数或者项目内的共同函数经常希望的。
void MyFunc(char *pReturn, size_t size)
{………
pReturn = (char *)malloc(sizeof(char) * num);………
}我们可以很明显地看出代码作者的意图,他想在函数调用处声明一个指针 char *pMyReturn=NULL;然后调用MyFunc处理并返回一段长度为size的一段动态内存。
那么作者能达到预期的效果吗?
那么我可以告诉作者,他的程序在编译期很幸运地通过了,可是在运行期他的程序崩溃终止。原因何在,是他触犯了系统不可侵犯的条款:错误地操作内存。
二、内存操作及问题相关知识点
为了能彻底解决动态内存传递的问题,我们先回顾一下内存管理的知识要点。
(1)内存分配方式有三种:
从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活。
(2)指针的操作流程
申请并初始化或设置为空:
int *pInt=NULL;开辟空间或者使其指向对象:
pInt=new Int(3);或者int i=3;pint=&i;用指针(更确切地说是操作内存,在使用之前加if(pint!=NULL)或者assert(pInt!=NULL)后再使用,以防内存申请失败的情况下使用指针):
if(p!=NULL) {use pint};释放使用完的内存
free(pInt);置指针为空
pInt=NULL;(避免野指针的出现)
(3)在函数的参数传递中,编译器总是要为函数的每个参数制作临时副本,如果参数为p的话,那么编译器会产生p的副本_p,使_p=p; 如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。
三、问题分析
根据上面的规则我们可以很容易分析例子中失败的原因。
void MyFunc(char *pReturn, size_t size)
{………
pReturn = (char *)malloc(sizeof(char) * num);………
} void main(void){ char *pMyReturn=NULL;MyFunc(pMyReturn,10);}在MyFunc(char *pReturn, size_t size)中_pMyReturn真实地申请到了内存, pMyReturn申请了新的内存,只是把_pMyReturn 所指的内存地址改变了,但是pMyReturn丝毫未变。所以函数MyFunc并不能输出任何东西。事实上,每执行一次MyFunc就会泄露一块内存,因为没有用free释放内存。
四、问题解决方案
函数间传递动态数据我们可以有三种解决方法。
方法一:如果我们是用C++编程,我们可以很方便地利用引用这个技术。我也极力推荐你用引用,因为它会使你少犯一些错误。以下是一个例子。
void MyFunc(char* &pReturn,size_t size){ pReturn=(char*)malloc(size);memset(pReturn,0x00,size);if(size>=13)
strcpy(pReturn,"Hello World!");}
void main(){ char *pMyReturn=NULL;MyFunc(pMyReturn,15);if(pMyReturn!=NULL)
{ char *pTemp=pMyReturn;while(*pTemp!=''\0'')
cout<<*pTemp++;pTemp=NULL;strcpy(pMyReturn,"AAAAAAAA");free(pMyReturn);pMyReturn=NULL;}方法二:利用二级指针
void MyFunc (char ** pReturn, size_t size)
{ * pReturn = (char *)malloc(size);} void main(void)
{ char * pMyReturn = NULL;MyFunc (&pMyReturn, 100);// 注意参数是 & pMyReturn if(pMyReturn!=NULL){ strcpy(pMyReturn, "hello");cout<< pMyReturn << endl;free(pMyReturn);pMyReturn=NULL;}}为什么二级指针就可以了。原因通过函数传递规则可以很容易地分析出来。我们将& pMyReturn传递了进去,就是将双重指针的内容传递到了函数中。函数过程利用改变指针的内容,这样pMyReturn很明显指向了开辟的内存 .
方法三:用函数返回值来传递动态内存
char * MyFunc (void)
{ char *p =new char[20];memset(p,0x00,sizeof(p));return p;} void main(void)
{ char *str = NULL;str = MyFunc();if(str!=NULL)
{ strcpy(str,"Hello,baby");cout<< str << endl;free(str);str=NULL;}请注意的是函数写成这样的话,你是不能返回什么动态内存的,因为p指向的是字符串常量。内存在位于静态存储区上分配,你无法改变。(你想要得到动态内存我们一定要看到malloc或者new)。
char * MyFunc (void)
{ char *p =“Hello World”
return p;}结束语
操作内存是C/C++一个难点,我们作为专业的软件开发人员。应该深入理解并能灵活地掌握指针和内存的操作。