关于头文件中的 static inline函数

关于头文件中的 static inline函数
    头文件中常见static inline函数,于是思考有可能遇到的问题,如头文件经常会被包含会不会产生很多副本?网上说法不一。于是自己验证。 经过arm-none-eabi-gcc下测试后得出结论。

    inline 关键字实际上仅是 建议内联并不强制内联,gcc中O0优化时是不内联的,即使是O2以上,如果该函数被作为函数指针赋值,那么他也不会内联,也必须产生函数实体,以获得该函数地址。经测试c文件中的仅inline函数即使Os优化也不内联,因为没有static,编译认他是全局的,因此像普通函数一样编译了,本c文件也一样通过 bl inline_func 这样的方式调用,不像网上别人说的,本c会内联,其他c文件则通过bl inline_func 方式。加入static 后则内联了。(Os优化等级测试)
    所以在头文件中用inline时务必加入static,否则当inline不内联时就和普通函数在头文件中定义一样,当多个c文件包含时就会重定义。所以加入static代码健壮性高,如果都内联了实际效果上是一样的。(gcc下验证过O0级别includes.h中仅定义inline的函数,编译失败,Os编译成功)

为什么要在头文件中定义函数呢?
虽然知道了头文件中用inline函数时要加入static,但是为什么要在头文件中定义函数呢?
一些简单的封装接口函数,如 open() { vfs_open() } 仅仅是为了封装一个接口,我们不希望耗费一次函数调用的时间,解决方法一是宏,但是作为接口,宏不够清晰。那选择inline,但是如果在c文件中写
main.c
inline void open(void)
{
    vfs_open();
头文件加声明,外部要使用则不会内联的,因为编译器有个原则,以c文件为单位进行逐个编译obj,每个c文件的编译是独立的,该c文件用到的外部函数都在编译时预留一个符号,只有等到所有obj生成后链接时才给这些符号地址(链接脚本决定地址),所以其他c文件编译时只会看到这个函数的声明而无法知道她的实体,就会像普通函数一样通过bl 一个函数地址,等链接的时候再填入该地址了,他做不到内联展开。
所以要内联则必须在每个用到它的c文件体现实体,那就只有在头文件了, 所以会把这类希望全局使用又希望增加效率的函数实现在头文件中static inline

static inline 的坏处
    因为inline 是C99才有的关键字,C89没有,有部分编译器不支持,或者部分支持,如支持__inline 或 __inline__等,所以我们一般会用一个宏定义inline 如:
#define INLINE    static inline
不支持inline时:
#define INLINE    static
    但是这样如果编译器不支持inline 即意味着之前 static inline的函数全部被修改为 static,在头文件中写static会有什么后果呢?
经过测试果然和我们想的一样,每个c文件包含了该头文件后全部都有了该函数副本。这无疑增大了很多代码量。比如在include.h
这样的大头文件,几乎每个c文件我们都会包含他,相当于每一C文件都会加入一个 static void func(void){...}  实体。如果是函数宏则不会有这种问题,函数宏是没有实际代码的,没调用他时代码不存在。这就是头文件中用static inline 函数的坏处。但是可以通过优化解决,经过测试,O0优化下在头文件中定义static 函数包含该头文件的三个c文件的确都有了该函数,但是在Os优化下则只有调用了该函数的C文件才有实体。这是由编译器对static函数的特性决定的。总之他的法则和我们想的一致,就是头文件仅仅是单纯的展开,而每个C独立编译,不会因为知道其他个C文件定义了该函数,这个c文件就把他当外部bl了。



关于c文件中的static函数
static函数除了文件内使用这个功能外,在优化上也有作用,static定义后如果该文件没有函数调用他,那么他意味着没有用,其他文件不可能调用,所以开优化就被优化掉了(已验证),无优化时则还在。这点一定要注意中断函数最好不要写static,中断函数如果没有中断服务函数的,即没有被调用,static可能被优化,当然也不一定,因为没有中间服务函数的独立中断服务函数必须在链接脚本体现,可能需要再链接脚本上加入KEEP参数应该就没问题。

这里排除非gcc编译器,如code worrior编译器则不同,code worrior是定制型的,通过识别main函数,因此他有主函数枝干可以判断哪些函数被调用,哪些是无用的,但是gcc不同,没有所谓的main,所以三个c文件如果没有static的函数,即使开优化也都会编译出来,他并不知道哪些函数有用,哪些函数无用。而static后他就一定知道他没被调用即无用,所以可以优化掉。

以上均指arm-none-eabi-gcc环境下测试的结论,其他编译器有些不同。但是c语言的编译规则还是以gcc为标准,即使其他编译器有些不同我们平时编程时还是以这个为标准。
在codeworrior 编译器上测试,即使0优化依然会把inline内联,有main函数,没有被主干调用的都是无用函数全部被标记UNUSED,不编译,因此上面讨论的当不支持inline时static函数实体过多的问题它也不存在。


  • 41
    点赞
  • 136
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
目录树 下面再给个样例 ├─Makefile │ ├─boot │ bootsect.s │ head.s │ setup.s │ ├─fs │ bitmap.c │ block_dev.c │ buffer.c │ char_dev.c │ exec.c │ fcntl.c │ file_dev.c │ file_table.c │ inode.c │ ioctl.c │ Makefile │ namei.c │ open.c │ pipe.c │ read_write.c │ stat.c │ super.c │ truncate.c │ ├─include │ │ a.out.h │ │ const.h │ │ ctype.h │ │ errno.h │ │ fcntl.h │ │ signal.h │ │ stdarg.h │ │ stddef.h │ │ string.h │ │ termios.h │ │ time.h │ │ unistd.h │ │ utime.h │ │ │ ├─asm │ │ io.h │ │ memory.h │ │ segment.h │ │ system.h │ │ │ ├─linux │ │ config.h │ │ fs.h │ │ hdreg.h │ │ head.h │ │ kernel.h │ │ mm.h │ │ sched.h │ │ sys.h │ │ tty.h │ │ │ └─sys │ stat.h │ times.h │ types.h │ utsname.h │ wait.h │ ├─init │ main.c │ ├─kernel │ │ asm.s │ │ exit.c │ │ fork.c │ │ mktime.c │ │ panic.c │ │ printk.c │ │ sched.c │ │ signal.c │ │ sys.c │ │ system_call.s │ │ vsprintf.c │ │ │ ├─blk_drv │ │ blk.h │ │ floppy.c │ │ hd.c │ │ ll_rw_blk.c │ │ Makefile │ │ ramdisk.c │ │ │ ├─chr_drv │ │ console.c │ │ keyboard.S │ │ Makefile │ │ rs_io.s │ │ serial.c │ │ tty_io.c │ │ tty_ioctl.c │ │ │ └─math │ Makefile │ math_emulate. │ ├─lib │ close.c │ ctype.c │ dup.c │ errno.c │ execve.c │ Makefile │ malloc.c │ open.c │ setsid.c │ string.c │ wait.c │ write.c │ _exit.c │ ├─mm │ Makefile │ memory.c │ page.s │ └─tools build.c 样例 main。c 用sourceinsight软件阅读 很方便 /* * linux/init/main.c * * (C) 1991 Linus Torvalds */ #define __LIBRARY__ // 定义该变量是为了包括定义在unistd.h 的内嵌汇编代码等信息。 #include // *.h 头文件所在的默认目录是include/,则在代码就不用明确指明位置。 // 如果不是UNIX 的标准头文件,则需要指明所在的目录,并用双引号括住。 // 标准符号常数与类型文件。定义了各种符号常数和类型,并申明了各种函数。 // 如果定义了__LIBRARY__,则还包括系统调用号和内嵌汇编代码_syscall0()等。 #include // 时间类型头文件。其最主要定义了tm 结构和一些有关时间的函数原形。 /* * we need this inline - forking from kernel space will result * in NO COPY ON WRITE (!!!), until an execve is executed. This * is no problem, but for the stack. This is handled by not letting * main() use the stack at all after fork(). Thus, no function * calls - which means inline code for fork too, as otherwise we * would use the stack upon exit from 'fork()'. * * Actually only pause and fork are needed inline, so that there * won't be any messing with the stack from main(), but we define * some others too. */ /* * 我们需要下面这些内嵌语句 - 从内核空间创建进程(forking)将导致没有写时复制(COPY ON WRITE)!!! * 直到一个执行execve 调用。这对堆栈可能带来问题。处理的方法是在fork()调用之后不让main()使用 * 任何堆栈。因此就不能有函数调用 - 这意味着fork 也要使用内嵌的代码,否则我们在从fork()退出 * 时就要使用堆栈了。 * 实际上只有pause 和fork 需要使用内嵌方式,以保证从main()不会弄乱堆栈,但是我们同时还 * 定义了其它一些函数。 */ static inline _syscall0 (int, fork) // 是unistd.h 的内嵌宏代码。以嵌入汇编的形式调用 // Linux 的系统调用断0x80。该断是所有系统调用的 // 入口。该条语句实际上是int fork()创建进程系统调用。 // syscall0 名称最后的0 表示无参数,1 表示1 个参数。 static inline _syscall0 (int, pause) // int pause()系统调用:暂停进程的执行,直到 // 收到一个信号。 static inline _syscall1 (int, setup, void *, BIOS) // int setup(void * BIOS)系统调用,仅用于 // linux 初始化(仅在这个程序被调用)。 static inline _syscall0 (int, sync) // int sync()系统调用:更新文件系统。 #include // tty 头文件,定义了有关tty_io,串行通信方面的参数、常数。 #include // 调度程序头文件,定义了任务结构task_struct、第1 个初始任务 // 的数据。还有一些以宏的形式定义的有关描述符参数设置和获取的 // 嵌入式汇编函数程序。 #include // head 头文件,定义了段描述符的简单结构,和几个选择符常量。 #include // 系统头文件。以宏的形式定义了许多有关设置或修改 // 描述符/断门等的嵌入式汇编子程序。 #include // io 头文件。以宏的嵌入汇编程序形式定义对io 端口操作的函数。 #include // 标准定义头文件。定义了NULL, offsetof(TYPE, MEMBER)。 #include // 标准参数头文件。以宏的形式定义变量参数列表。主要说明了-个 // 类型(va_list)和三个宏(va_start, va_arg 和va_end),vsprintf、 // vprintf、vfprintf。 #include #include // 文件控制头文件。用于文件及其描述符的操作控制常数符号的定义。 #include // 类型头文件。定义了基本的系统数据类型。 #include // 文件系统头文件。定义文件表结构(file,buffer_head,m_inode 等)。 static char printbuf[1024]; // 静态字符串数组。 extern int vsprintf (); // 送格式化输出到一字符串(在kernel/vsprintf.c,92 行)。 extern void init (void); // 函数原形,初始化(在168 行)。 extern void blk_dev_init (void); // 块设备初始化子程序(kernel/blk_drv/ll_rw_blk.c,157 行) extern void chr_dev_init (void); // 字符设备初始化(kernel/chr_drv/tty_io.c, 347 行) extern void hd_init (void); // 硬盘初始化程序(kernel/blk_drv/hd.c, 343 行) extern void floppy_init (void); // 软驱初始化程序(kernel/blk_drv/floppy.c, 457 行) extern void mem_init (long start, long end); // 内存管理初始化(mm/memory.c, 399 行) extern long rd_init (long mem_start, int length); //虚拟盘初始化(kernel/blk_drv/ramdisk.c,52) extern long kernel_mktime (struct tm *tm); // 建立内核时间(秒)。 extern long startup_time; // 内核启动时间(开机时间)(秒)。 /* * This is set up by the setup-routine at boot-time */ /* * 以下这些数据是由setup.s 程序在引导时间设置的(参见第2 章2.3.1 节的表2.1)。 */ #define EXT_MEM_K (*(unsigned short *)0x90002) // 1M 以后的扩展内存大小(KB)。 #define DRIVE_INFO (*(struct drive_info *)0x90080) // 硬盘参数表基址。 #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC) // 根文件系统所在设备号。 /* * Yeah, yeah, it's ugly, but I cannot find how to do this correctly * and this seems to work. I anybody has more info on the real-time * clock I'd be interested. Most of this was trial and error, and some * bios-listing reading. Urghh. */ /* * 是啊,是啊,下面这段程序很差劲,但我不知道如何正确地实现,而且好象它还能运行。如果有 * 关于实时时钟更多的资料,那我很感兴趣。这些都是试探出来的,以及看了一些bios 程序,呵! */ #define CMOS_READ(addr) ({ \ // 这段宏读取CMOS 实时时钟信息。 outb_p (0x80 | addr, 0x70); \ // 0x70 是写端口号,0x80|addr 是要读取的CMOS 内存地址。 inb_p (0x71); \ // 0x71 是读端口号。 } ) #define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10) // 将BCD 码转换成数字。 static void time_init (void) // 该子程序取CMOS 时钟,并设置开机时间??startup_time(秒)。 { struct tm time; do { time.tm_sec = CMOS_READ (0); // 参见后面CMOS 内存列表。 time.tm_min = CMOS_READ (2); time.tm_hour = CMOS_READ (4); time.tm_mday = CMOS_READ (7); time.tm_mon = CMOS_READ (8); time.tm_year = CMOS_READ (9); } while (time.tm_sec != CMOS_READ (0)); BCD_TO_BIN (time.tm_sec); BCD_TO_BIN (time.tm_min); BCD_TO_BIN (time.tm_hour); BCD_TO_BIN (time.tm_mday); BCD_TO_BIN (time.tm_mon); BCD_TO_BIN (time.tm_year); time.tm_mon--; startup_time = kernel_mktime (&time); } static long memory_end = 0; // 机器具有的内存(字节数)。 static long buffer_memory_end = 0; // 高速缓冲区末端地址。 static long main_memory_start = 0; // 主内存(将用于分页)开始的位置。 struct drive_info { char dummy[32]; } drive_info; // 用于存放硬盘参数表信息。 void main (void) /* This really IS void, no error here. */ { /* The startup routine assumes (well, ...) this */ /* 这里确实是void,并没错。在startup 程序(head.s)就是这样假设的。 */ // 参见head.s 程序第136 行开始的几行代码。 /* * Interrupts are still disabled. Do necessary setups, then * enable them */ /* * 此时断仍被禁止着,做完必要的设置后就将其开启。 */ // 下面这段代码用于保存: // 根设备号 ??ROOT_DEV; 高速缓存末端地址??buffer_memory_end; // 机器内存数??memory_end;主内存开始地址 ??main_memory_start; ROOT_DEV = ORIG_ROOT_DEV; drive_info = DRIVE_INFO; memory_end = (1 << 20) + (EXT_MEM_K < 16 * 1024 * 1024) // 如果内存超过16Mb,则按16Mb 计。 memory_end = 16 * 1024 * 1024; if (memory_end > 12 * 1024 * 1024) // 如果内存>12Mb,则设置缓冲区末端=4Mb buffer_memory_end = 4 * 1024 * 1024; else if (memory_end > 6 * 1024 * 1024) // 否则如果内存>6Mb,则设置缓冲区末端=2Mb buffer_memory_end = 2 * 1024 * 1024; else buffer_memory_end = 1 * 1024 * 1024; // 否则则设置缓冲区末端=1Mb main_memory_start = buffer_memory_end; // 主内存起始位置=缓冲区末端; #ifdef RAMDISK // 如果定义了虚拟盘,则主内存将减少。 main_memory_start += rd_init (main_memory_start, RAMDISK * 1024); #endif // 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,实在看 // 不下去了,就先放一放,看下一个初始化调用 -- 这是经验之谈?。 mem_init (main_memory_start, memory_end); trap_init (); // 陷阱门(硬件断向量)初始化。(kernel/traps.c,181 行) blk_dev_init (); // 块设备初始化。 (kernel/blk_dev/ll_rw_blk.c,157 行) chr_dev_init (); // 字符设备初始化。 (kernel/chr_dev/tty_io.c,347 行) tty_init (); // tty 初始化。 (kernel/chr_dev/tty_io.c,105 行) time_init (); // 设置开机启动时间??startup_time(见76 行)。 sched_init (); // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c,385) buffer_init (buffer_memory_end); // 缓冲管理初始化,建内存链表等。(fs/buffer.c,348) hd_init (); // 硬盘初始化。 (kernel/blk_dev/hd.c,343 行) floppy_init (); // 软驱初始化。 (kernel/blk_dev/floppy.c,457 行) sti (); // 所有初始化工作都做完了,开启断。 // 下面过程通过在堆栈设置的参数,利用断返回指令切换到任务0。 move_to_user_mode (); // 移到用户模式。 (include/asm/system.h,第1 行) if (!fork ()) { /* we count on this going ok */ init (); } /* * NOTE!! For any other task 'pause()' would mean we have to get a * signal to awaken, but task0 is the sole exception (see 'schedule()') * as task 0 gets activated at every idle moment (when no other tasks * can run). For task0 'pause()' just means we go check if some other * task can run, and if not we return here. */ /* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返 * 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任务0 在 * 任何空闲时间里都会被激活(当没有其它任务在运行时),因此对于任务0'pause()'仅意味着 * 我们返回来查看是否有其它任务可以运行,如果没有的话我们就回到这里,一直循环执行'pause()'。 */ for (;;) pause (); } static int printf (const char *fmt, ...) // 产生格式化信息并输出到标准输出设备stdout(1),这里是指屏幕上显示。参数'*fmt'指定输出将 // 采用的格式,参见各种标准C 语言书籍。该子程序正好是vsprintf 如何使用的一个例子。 // 该程序使用vsprintf()将格式化的字符串放入printbuf 缓冲区,然后用write()将缓冲区的内容 // 输出到标准设备(1--stdout)。 { va_list args; int i; va_start (args, fmt); write (1, printbuf, i = vsprintf (printbuf, fmt, args)); va_end (args); return i; } static char *argv_rc[] = { "/bin/sh", NULL}; // 调用执行程序时参数的字符串数组。 static char *envp_rc[] = { "HOME=/", NULL}; // 调用执行程序时的环境字符串数组。 static char *argv[] = { "-/bin/sh", NULL}; // 同上。 static char *envp[] = { "HOME=/usr/root", NULL}; void init (void) { int pid, i; // 读取硬盘参数包括分区表信息并建立虚拟盘和安装根文件系统设备。 // 该函数是在25 行上的宏定义的,对应函数是sys_setup(),在kernel/blk_drv/hd.c,71 行。 setup ((void *) &drive_info); (void) open ("/dev/tty0", O_RDWR, 0); // 用读写访问方式打开设备“/dev/tty0”, // 这里对应终端控制台。 // 返回的句柄号0 -- stdin 标准输入设备。 (void) dup (0); // 复制句柄,产生句柄1 号 -- stdout 标准输出设备。 (void) dup (0); // 复制句柄,产生句柄2 号 -- stderr 标准出错输出设备。 printf ("%d buffers = %d bytes buffer space\n\r", NR_BUFFERS, NR_BUFFERS * BLOCK_SIZE); // 打印缓冲区块数和总字节数,每块1024 字节。 printf ("Free mem: %d bytes\n\r", memory_end - main_memory_start); //空闲内存字节数。 // 下面fork()用于创建一个子进程(子任务)。对于被创建的子进程,fork()将返回0 值, // 对于原(父进程)将返回子进程的进程号。所以180-184 句是子进程执行的内容。该子进程 // 关闭了句柄0(stdin),以只读方式打开/etc/rc 文件,并执行/bin/sh 程序,所带参数和 // 环境变量分别由argv_rc 和envp_rc 数组给出。参见后面的描述。 if (!(pid = fork ())) { close (0); if (open ("/etc/rc", O_RDONLY, 0)) _exit (1); // 如果打开文件失败,则退出(/lib/_exit.c,10)。 execve ("/bin/sh", argv_rc, envp_rc); // 装入/bin/sh 程序并执行。 _exit (2); // 若execve()执行失败则退出(出错码2,“文件或目录不存在”)。 } // 下面是父进程执行的语句。wait()是等待子进程停止或终止,其返回值应是子进程的进程号(pid)。 // 这三句的作用是父进程等待子进程的结束。&i 是存放返回状态信息的位置。如果wait()返回值不 // 等于子进程号,则继续等待。 if (pid > 0) while (pid != wait (&i)) /* nothing */ ; // 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环首先再创建一个子进程, // 如果出错,则显示“初始化程序创建子进程失败”的信息并继续执行。对于所创建的子进程关闭所有 // 以前还遗留的句柄(stdin, stdout, stderr),新创建一个会话并设置进程组号,然后重新打开 // /dev/tty0 作为stdin,并复制成stdout 和stderr。再次执行系统解释程序/bin/sh。但这次执行所 // 选用的参数和环境数组另选了一套(见上面165-167 行)。然后父进程再次运行wait()等待。如果 // 子进程又停止了执行,则在标准输出上显示出错信息“子进程pid 停止了运行,返回码是i”,然后 // 继续重试下去…,形成“大”死循环。 while (1) { if ((pid = fork ()) < 0) { printf ("Fork failed in init\r\n"); continue; } if (!pid) { close (0); close (1); close (2); setsid (); (void) open ("/dev/tty0", O_RDWR, 0); (void) dup (0); (void) dup (0); _exit (execve ("/bin/sh", argv, envp)); } while (1) if (pid == wait (&i)) break; printf ("\n\rchild %d died with code %04x\n\r", pid, i); sync (); } _exit (0); /* NOTE! _exit, not exit() */ }
实验三 移植U-Boot-1.3.1 实验 【实验目的】 了解 U-Boot-1.3.1 的代码结构,掌握其移植方法。 【实验环境】 1、Ubuntu 7.0.4发行版 2、u-boot-1.3.1 3、FS2410平台 4、交叉编译器 arm-softfloat-linux-gnu-gcc-3.4.5 【实验步骤】 一、建立自己的平台类型 (1)解压文件 #tar jxvf u-boot-1.3.1.tar.bz2 (2)进入 U-Boot源码目录 #cd u-boot-1.3.1 (3)创建自己的开发板: #cd board #cp smdk2410 fs2410 –a #cd fs2410 #mv smdk2410.c fs2410.c #vi Makefile (将 smdk2410修改为 fs2410) #cd ../../include/configs #cp smdk2410.h fs2410.h 退回 U-Boot根目录:#cd ../../ (4)建立编译选项 #vi Makefile smdk2410_config : unconfig @$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 NULL s3c24x0 fs2410_config : unconfig @$(MKCONFIG) $(@:_config=) arm arm920t fs2410 NULL s3c24x0 arm: CPU的架构(ARCH) arm920t: CPU的类型(CPU),其对应于 cpu/arm920t子目录。 fs2410: 开发板的型号(BOARD),对应于 board/fs2410目录。 NULL: 开发者/或经销商(vender),本例为空 s3c24x0: 片上系统(SOC) (5)编译 #make fs2410_config; #make 本步骤将编译 u-boot.bin文件,但此时还无法运行在FS2410开发板上。 二、修改 cpu/arm920t/start.S文件,完成 U-Boot的重定向 (1)修改断禁止部分 # if defined(CONFIG_S3C2410) ldr r1, =0x7ff /*根据 2410 芯片手册,INTSUBMSK 有 11位可用 */ ldr r0, =INTSUBMSK Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) str r1, [r0] # endif (2)修改时钟设置(这个文件要根据具体的平台进行修改) (3)将从Nor Flash启动改成从 NAND Flash启动 在文件找到 195-201 代码,并在 201行后面添加如下代码: 195 copy_loop: 196 ldmia r0!, {r3-r10} /* copy from source address [r0] */ 197 stmiar1!, {r3-r10} /* copy to target address [r1] */ 198 cmp r0, r2 /* until source end addreee [r2] */ 199 ble copy_loop 200 #endif /* CONFIG_SKIP_RELOCATE_UBOOT */ 201 #endif #ifdef CONFIG_S3C2410_NAND_BOOT @ reset NAND mov r1, #NAND_CTL_BASE ldr r2, =0xf830 @ initial value str r2, [r1, #oNFCONF] ldr r2, [r1, #oNFCONF] bic r2, r2, #0x800 @ enable chip str r2, [r1, #oNFCONF] mov r2, #0xff @ RESET command strb r2, [r1, #oNFCMD] mov r3, #0 @ wait nand1: add r3, r3, #0x1 cmp r3, #0xa blt nand1 nand2: ldr r2, [r1, #oNFSTAT] @ wait ready tst r2, #0x1 beq nand2 ldr r2, [r1, #oNFCONF] orr r2, r2, #0x800 @ disable chip str r2, [r1, #oNFCONF] @ get read to call C functions (for nand_read()) ldr sp, DW_STACK_START @ setup stack pointer mov fp, #0 @ no previous frame, so fp=0 @ copy U-Boot to RAM ldr r0, =TEXT_BASE mov r1, #0x0 mov r2, #0x30000 bl nand_read_ll tst r0, #0x0 beq ok_nand_read Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)bad_nand_read: loop2: b loop2 @ infinite loop ok_nand_read: @ verify mov r0, #0 ldr r1, =TEXT_BASE mov r2, #0x400 @ 4 bytes * 1024 = 4K-bytes go_next: ldr r3, [r0], #4 ldr r4, [r1], #4 teq r3, r4 bne notmatch subs r2, r2, #4 beq stack_setup bne go_next notmatch: loop3: b loop3 @ infinite loop #endif @ CONFIG_S3C2410_NAND_BOOT (4)在 “ _start_armboot: .word start_armboot ”后加入: .align 2 DW_STACK_START: .word STACK_BASE+STACK_SIZE-4 三、创建 board/fs2410/nand_read.c 文件,加入读 NAND Flash 的操作。 #include #define __REGb(x) (*(volatile unsigned char *)(x)) #define __REGi(x) (*(volatile unsigned int *)(x)) #define NF_BASE 0x4e000000 # if defined(CONFIG_S3C2410) #define NFCONF __REGi(NF_BASE + 0x0) #define NFCMD __REGb(NF_BASE + 0x4) #define NFADDR __REGb(NF_BASE + 0x8) #define NFDATA __REGb(NF_BASE + 0xc) #define NFSTAT __REGb(NF_BASE + 0x10) #define BUSY 1 inline void wait_idle(void) { int i; while(!(NFSTAT & BUSY)) for(i=0; i<10; i++); } /* low level nand read function */ int nand_read_ll(unsigned char *buf, unsigned long start_addr, int size) { int i, j; Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) if ((start_addr & NAND_BLOCK_MASK) || (size & NAND_BLOCK_MASK)) { return -1; /* invalid alignment */ } /* chip Enable */ NFCONF &= ~0x800; for(i=0; i<10; i++); for(i=start_addr; i > 9) & 0xff; NFADDR = (i >> 17) & 0xff; NFADDR = (i >> 25) & 0xff; wait_idle(); for(j=0; j NFCONF = conf; } Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)static inline void NF_Cmd(u8 cmd) { S3C2410_NAND * const nand = S3C2410_GetBase_NAND(); nand->NFCMD = cmd; } static inline void NF_CmdW(u8 cmd) { NF_Cmd(cmd); udelay(1); } static inline void NF_Addr(u8 addr) { S3C2410_NAND * const nand = S3C2410_GetBase_NAND(); nand->NFADDR = addr; } static inline void NF_WaitRB(void) { S3C2410_NAND * const nand = S3C2410_GetBase_NAND(); while (!(nand->NFSTAT & (1<NFDATA = data; } static inline u8 NF_Read(void) { S3C2410_NAND * const nand = S3C2410_GetBase_NAND(); return(nand->NFDATA); } static inline u32 NF_Read_ECC(void) { S3C2410_NAND * const nand = S3C2410_GetBase_NAND(); return(nand->NFECC); } static inline void NF_SetCE(NFCE_STATE s) { S3C2410_NAND * const nand = S3C2410_GetBase_NAND(); switch (s) { case NFCE_LOW: nand->NFCONF &= ~(1<NFCONF |= (1<NFCONF |= (1<<12); } extern ulong nand_probe(ulong physadr); static inline void NF_Reset(void) { int i; NF_SetCE(NFCE_LOW); NF_Cmd(0xFF); /* reset command */ for(i = 0; i < 10; i++); /* tWB = 100ns. */ NF_WaitRB(); /* wait 200~500us; */ NF_SetCE(NFCE_HIGH); } static inline void NF_Init(void) { #if 0 #define TACLS 0 #define TWRPH0 3 #define TWRPH1 0 #else #define TACLS 0 #define TWRPH0 4 #define TWRPH1 2 #endif #if defined(CONFIG_S3C2440) NF_Conf((TACLS<<12)|(TWRPH0<<8)|(TWRPH1<<4)); NF_Cont((1<<6)|(1<<4)|(1<<1)|(1<<0)); #else NF_Conf((1<<15)|(0<<14)|(0<<13)|(1<<12)|(1<<11)|(TACLS<<8)|(TWRPH0<<4)|(TWRPH1<NFCONF = (1<<15)|(1<<14)|(1<<13)|(1<<12)|(1<<11)|(TACLS<<8)|(TWRPH0<<4)|(TWRPH1<> 20); } #endif (2)配置GPIO 和 PLL 根据开发板的硬件说明和芯片手册,修改GPIO 和 PLL的配置。 六、修改 include/configs/fs2410.h 头文件 (1)加入命令定义 /* Command line configuration. */ #include #define CONFIG_CMD_ASKENV #define CONFIG_CMD_CACHE #define CONFIG_CMD_DATE #define CONFIG_CMD_DHCP #define CONFIG_CMD_ELF #define CONFIG_CMD_PING #define CONFIG_CMD_NAND #define CONFIG_CMD_REGINFO #define CONFIG_CMD_USB #define CONFIG_CMD_FAT (2)修改命令提示符 #define CFG_PROMPT "SMDK2410 # " -> #define CFG_PROMPT "FS2410# " (3)修改默认载入地址 #define CFG_LOAD_ADDR 0x33000000 -> #define CFG_LOAD_ADDR 0x30008000 (4)加入 Flash环境信息 #define CFG_ENV_IS_IN_NAND 1 #define CFG_ENV_OFFSET 0X30000 #define CFG_NAND_LEGACY //#define CFG_ENV_IS_IN_FLASH 1 #define CFG_ENV_SIZE 0x10000 /* Total Size of Environment Sector */ (5)加入Nand Flash设置(在文件结尾处) /* NAND flash settings */ #if defined(CONFIG_CMD_NAND) #define CFG_NAND_BASE 0x4E000000 /* NandFlash控制器在SFR区起始寄存器地址 */ #define CFG_MAX_NAND_DEVICE 1 /* 支持的最在Nand Flash数据 */ #define SECTORSIZE 512 /* 1页的大小 */ #define NAND_SECTOR_SIZE SECTORSIZE Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)#define NAND_BLOCK_MASK 511 /* 页掩码 */ #define ADDR_COLUMN 1 /* 一个字节的Column地址 */ #define ADDR_PAGE 3 /* 3字节的页块地址!!!!!*/ #define ADDR_COLUMN_PAGE 4 /* 总共4字节的页块地址!!!!! */ #define NAND_ChipID_UNKNOWN 0x00 /* 未知芯片的ID号 */ #define NAND_MAX_FLOORS 1 #define NAND_MAX_CHIPS 1 /* Nand Flash命令层底层接口函数 */ #define WRITE_NAND_ADDRESS(d, adr) {rNFADDR = d;} #define WRITE_NAND(d, adr) {rNFDATA = d;} #define READ_NAND(adr) (rNFDATA) #define NAND_WAIT_READY(nand) {while(!(rNFSTAT&(1<<0)));} #define WRITE_NAND_COMMAND(d, adr) {rNFCMD = d;} #define WRITE_NAND_COMMANDW(d, adr) NF_CmdW(d) #define NAND_DISABLE_CE(nand) {rNFCONF |= (1<<11);} #define NAND_ENABLE_CE(nand) {rNFCONF &= ~(1<<11);} /* the following functions are NOP's because S3C24X0 handles this in hardware */ #define NAND_CTL_CLRALE(nandptr) #define NAND_CTL_SETALE(nandptr) #define NAND_CTL_CLRCLE(nandptr) #define NAND_CTL_SETCLE(nandptr) /* 允许 Nand Flash写校验 */ #define CONFIG_MTD_NAND_VERIFY_WRITE 1 (6)加入Nand Flash启动支持(在文件结尾处) /* Nandflash Boot*/ #define STACK_BASE 0x33f00000 #define STACK_SIZE 0x8000 /* NAND Flash Controller */ #define NAND_CTL_BASE 0x4E000000 #define bINT_CTL(Nb) __REG(INT_CTL_BASE + (Nb)) /* Offset */ #define oNFCONF 0x00 #define CONFIG_S3C2410_NAND_BOOT 1 /* Offset */ #define oNFCONF 0x00 #define oNFCMD 0x04 #define oNFADDR 0x08 #define oNFDATA 0x0c #define oNFSTAT 0x10 #define oNFECC 0x14 #define rNFCONF (*(volatile unsigned int *)0x4e000000) #define rNFCMD (*(volatile unsigned char *)0x4e000004) Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)#define rNFADDR (*(volatile unsigned char *)0x4e000008) #define rNFDATA (*(volatile unsigned char *)0x4e00000c) #define rNFSTAT (*(volatile unsigned int *)0x4e000010) #define rNFECC (*(volatile unsigned int *)0x4e000014) #define rNFECC0 (*(volatile unsigned char *)0x4e000014) #define rNFECC1 (*(volatile unsigned char *)0x4e000015) #define rNFECC2 (*(volatile unsigned char *)0x4e000016) #endif (7)加入 jffs2的支持 /*JFFS2 Support */ #undef CONFIG_JFFS2_CMDLINE #define CONFIG_JFFS2_NAND 1 #define CONFIG_JFFS2_DEV "nand0" #define CONFIG_JFFS2_PART_SIZE 0x4c0000 #define CONFIG_JFFS2_PART_OFFSET 0x40000 /*JFFS2 Support */ (8)加入 usb的支持 /* USB Support*/ #define CONFIG_USB_OHCI #define CONFIG_USB_STORAGE #define CONFIG_USB_KEYBOARD #define CONFIG_DOS_PARTITION #define CFG_DEVICE_DEREGISTER #define CONFIG_SUPPORT_VFAT #define LITTLEENDIAN /* USB Support*/ 七、修改 include/linux/mtd/nand.h头文件 屏蔽如下定义: #if 0 /* Select the chip by setting nCE to low */ #define NAND_CTL_SETNCE 1 /* Deselect the chip by setting nCE to high */ #define NAND_CTL_CLRNCE 2 /* Select the command latch by setting CLE to high */ #define NAND_CTL_SETCLE 3 /* Deselect the command latch by setting CLE to low */ #define NAND_CTL_CLRCLE 4 /* Select the address latch by setting ALE to high */ #define NAND_CTL_SETALE 5 /* Deselect the address latch by setting ALE to low */ #define NAND_CTL_CLRALE 6 /* Set write protection by setting WP to high. Not used! */ #define NAND_CTL_SETWP 7 /* Clear write protection by setting WP to low. Not used! */ Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)#define NAND_CTL_CLRWP 8 #endif 八、修改 include/linux/mtd/nand_ids.h 头文件 在该文件加入开发板的 NAND Flash型号 {"Samsung K9F1208U0B", NAND_MFR_SAMSUNG, 0x76, 26, 0, 4, 0x4000, 0}, 九、修改 common/env_nand.c文件 我们使用了早期的Nand读写方式,因此做出下列移植: (1) 加入函数原型定义 extern struct nand_chip nand_dev_desc[CFG_MAX_NAND_DEVICE]; extern int nand_legacy_erase(struct nand_chip *nand, size_t ofs, size_t len, int clean); /* info for NAND chips, defined in drivers/nand/nand.c */ extern nand_info_t nand_info[CFG_MAX_NAND_DEVICE]; (2) 修改saveenv函数 注释//if (nand_erase(&nand_info[0], CFG_ENV_OFFSET, CFG_ENV_SIZE)) 加入:if (nand_legacy_erase(nand_dev_desc + 0, CFG_ENV_OFFSET, CFG_ENV_SIZE, 0)) 注释//ret = nand_write(&nand_info[0], CFG_ENV_OFFSET, &total, (u_char*)env_ptr); 加入:ret = nand_legacy_rw(nand_dev_desc + 0,0x00 | 0x02, CFG_ENV_OFFSET, CFG_ENV_SIZE, &total, (u_char*)env_ptr); (3) 修改env_relocate_spec函数 注释//ret = nand_read(&nand_info[0], CFG_ENV_OFFSET, &total, (u_char*)env_ptr); 加入:ret = nand_legacy_rw(nand_dev_desc + 0, 0x01 | 0x02, CFG_ENV_OFFSET, CFG_ENV_SIZE, &total, (u_char*)env_ptr); 十、修改 common/cmd_boot.c 文件,添加内核启动参数设置 (1) 首先添加头文件#include (2) 修改do_go函数。具体修改为: int do_go (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[]) { #if defined(CONFIG_I386) DECLARE_GLOBAL_DATA_PTR; #endif ulong addr, rc; int rcode = 0; ///////////////////////////////////////////////////////////////////////// char *commandline = getenv("bootargs"); struct param_struct *my_params=(struct param_struct *)0x30000100; memset(my_params,0,sizeof(struct param_struct)); my_params->u1.s.page_size=4096; my_params->u1.s.nr_pages=0x4000000>>12; memcpy(my_params->commandline,commandline,strlen(commandline)+1); Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)/////////////////////////////////////////////////////////////////////// if (argc usage); return 1; } addr = simple_strtoul(argv[1], NULL, 16); printf ("## Starting application at 0x%08lX ...\n", addr); /* * pass address parameter as argv[0] (aka command name), * and all remaining args */ #if defined(CONFIG_I386) /* * x86 does not use a dedicated register to pass the pointer * to the global_data */ argv[0] = (char *)gd; #endif #if !defined(CONFIG_NIOS) //////////////////////////////////////////////////////////////////// __asm__( "mov r1, #193\n" "mov ip, #0\n" "mcr p15, 0, ip, c13, c0, 0\n" /* zero PID */ "mcr p15, 0, ip, c7, c7, 0\n" /* invalidate I,D caches */ "mcr p15, 0, ip, c7, c10, 4\n" /* drain write buffer */ "mcr p15, 0, ip, c8, c7, 0\n" /* invalidate I,D TLBs */ "mrc p15, 0, ip, c1, c0, 0\n" /* get control register */ "bic ip, ip, #0x0001\n" /* disable MMU */ "mov pc, %0\n" "nop\n" : :"r"(addr) ); ////////////////////////////////////////////////////////// rc = ((ulong (*)(int, char *[]))addr) (--argc, &argv[1]); #else /* * Nios function pointers are address >> 1 */ rc = ((ulong (*)(int, char *[]))(addr>>1)) (--argc, &argv[1]); #endif if (rc != 0) rcode = 1; Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) printf ("## Application terminated, rc = 0x%lX\n", rc); return rcode; } 其用//括起来的代码是要添加的代码。否则在引导LINUX 内核的时候会出现一个 Error: a 或无法传递内核启动参数的错误。其原因是平台号或启动参数没有正确传入内核。 十一、交叉编译 U-BOOT #make distclean #make fs2410_config export PATH=$PATH:/home/linux/crosstool/gcc-3.4.5-glibc-2.3.6/arm-softfloat-linux-gnu/bin: #make CROSS_COMPILE= arm-softfloat-linux-gnu- 生成的 u-boot.bin 即为我们移植后的结果。下载到开发板上运行! Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)U-Boot简介 U-Boot,全称 Universal Boot Loader,是遵循 GPL 条款的开放源码项目。从 FADSROM、 8xxROM、PPCBOOT 逐步发展演化而来。其源码目录、编译形式与 Linux 内核很相似,事 实上,不少U-Boot源码就是相应的 Linux内核源程序的简化,尤其是一些设备的驱动程序, 这从U-Boot源码的注释能体现这一点。 但是U-Boot不仅仅支持嵌入式Linux系统的引导, 当前,它还支持 NetBSD, VxWorks, QNX, RTEMS, ARTOS, LynxOS嵌入式操作系统。其目 前要支持的目标操作系统是OpenBSD, NetBSD, FreeBSD,4.4BSD, Linux, SVR4, Esix, Solaris, Irix, SCO, Dell, NCR, VxWorks, LynxOS, pSOS, QNX, RTEMS, ARTOS。这是 U-Boot Universal的一层含义,另外一层含义则是 U-Boot除了支持 PowerPC系列的处理器外,还能 支持 MIPS、 x86、ARM、NIOS、XScale等诸多常用系列的处理器。这两个特点正是U-Boot 项目的开发目标,即支持尽可能多的嵌入式处理器和嵌入式操作系统。就目前来看,U-Boot 对 PowerPC 系列处理器支持最为丰富,对 Linux 的支持最完善。其它系列的处理器和操作 系统基本是在2002年11 月PPCBOOT改名为U-Boot后逐步扩充的。 从PPCBOOT向U-Boot 的顺利过渡,很大程度上归功于 U-Boot 的维护人德国 DENX 软件工程心 Wolfgang Denk[以下简称 W.D]本人精湛专业水平和持着不懈的努力。当前,U-Boot 项目正在他的领 军之下,众多有志于开放源码 BOOT LOADER移植工作的嵌入式开发人员正如火如荼地将 各个不同系列嵌入式处理器的移植工作不断展开和深入, 以支持更多的嵌入式操作系统的装 载与引导。 选择 U-Boot的理由: ① 开放源码; ② 支持多种嵌入式操作系统内核,如 Linux、NetBSD, VxWorks, QNX, RTEMS, ARTOS, LynxOS; ③ 支持多个处理器系列,如 PowerPC、ARM、x86、MIPS、XScale; ④ 较高的可靠性和稳定性; ④ 较高的可靠性和稳定性; ⑤ 高度灵活的功能设置,适合U-Boot调试、操作系统不同引导要求、产品发布等; ⑥ 丰富的设备驱动源码,如串口、以太网、SDRAM、FLASH、LCD、NVRAM、EEPROM、 RTC、键盘等; ⑦ 较为丰富的开发调试文档与强大的网络技术支持; U-Boot主要目录结构 - board 目标板相关文件,主要包含 SDRAM、FLASH 驱动; - common 独立于处理器体系结构的通用代码,如内存大小探测与故障检测; - cpu 与处理器相关的文件。如 mpc8xx子目录下含串口、网口、LCD 驱动及断初始化等 文件; - driver 通用设备驱动,如 CFI FLASH 驱动(目前对INTEL FLASH 支持较好) Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)- doc U-Boot的说明文档; - examples可在 U-Boot下运行的示例程序;如 hello_world.c,timer.c; - include U-Boot头文件;尤其 configs子目录下与目标板相关的配置头文件是移植过程经 常要修改的文件; - lib_xxx 处理器体系相关的文件,如 lib_ppc, lib_arm目录分别包含与 PowerPC、ARM体系 结构相关的文件; - net 与网络功能相关的文件目录,如 bootp,nfs,tftp; - post 上电自检文件目录。尚有待于进一步完善; - rtc RTC驱动程序; - tools 用于创建 U-Boot S-RECORD 和 BIN 镜像文件的工具; U-Boot支持的主要功能 U-Boot可支持的主要功能列表 系统引导 支持NFS挂载、RAMDISK(压缩或非压缩)形式的根文件系统 支持 NFS挂载、从 FLASH 引导压缩或非压缩系统内核; 基本辅助功能 强大的操作系统接口功能;可灵活设置、传递多个关键参数给操作系统,适 合系统在不同开发阶段的调试要求与产品发布,尤对Linux支持最为强劲; 支持目标板环境参数多种存储方式,如 FLASH、NVRAM、EEPROM; CRC32校验,可校验 FLASH 内核、RAMDISK 镜像文件是否完好; 设备驱动 串口、 SDRAM、 FLASH、 以太网、 LCD、 NVRAM、 EEPROM、 键盘、 USB、 PCMCIA、 PCI、RTC等驱动支持; 上电自检功能 SDRAM、FLASH 大小自动检测;SDRAM故障检测;CPU型号; 特殊功能 XIP内核引导; 移植前的准备 (1)、首先读读 uboot自带的 readme文件,了解了一个大概。 (2)、看看 common.h,这个文件定义了一些基本的东西,并包含了一些必要的头文件。再 看看 flash.h,这个文件里面定义了 flash_info_t为一个 struct。包含了 flash的一些属性定义。 并且定义了所有的 flash 的属性,其,AMD 的有:AMD_ID_LV320B,定义为“#define AMD_ID_LV320B 0x22F922F9” 。 (3)、对于“./borad/at91rm9200dk/flash.c”的修改,有以下的方面: “void flash_identification(flash_info_t *info)”这个函数的目的是确认 flash的型号。注意的 是,这个函数里面有一些宏定义,直接读写了 flash。并获得 ID 号。 (4)、修改: ”./board/at91rm9200dk/config.mk”为 TEXT_BASE=0x21f80000 为 TEXT_BASE=0x21f00000 (当然,你应该根据自己的板子来 修改,和一级boot的定义的一致即可)。 Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)(5)、再修改”./include/configs/at91rm9200dk.h”为 修改 flash和 SDRAM的大小。 (6)、另外一个要修改的文件是: ./borad/at91rm9200dk/flash.c。这个文件修改的部分比较的多。 a. 首先是OrgDef的定义,加上目前的 flash。 b. 接下来,修改”#define FLASH_BANK_SIZE 0x200000”为自己flash的 容量 c. 在修改函数 flash_identification(flash_info_t * info)里面的打印信息,这部分将在 u-boot 启动的时候显示。 d. 然后修改函数 flash_init(void)里面对一些变量的赋值。 e. 最后修改的是函数 flash_print_info(flash_info_t * info)里面实际打印的函数信息。 f. 还有一个函数需要修改,就是: “flash_erase” ,这个函数要检测先前知道的 flash类型是 否匹配,否则,直接就返回了。把这里给注释掉。 (7)、接下来看看 SDRAM的修改。 这个里面对于“SIZE”的定义都是基于字节计算的。 只要修改”./include/configs/at91rm9200dk.h”里面的 “#define PHYS_SDRAM_SIZE 0X200000”就可以了。注意,SIZE 是以字节为单位的。 (8)、还有一个地方要注意 就是按照目前的设定,一级 boot 把 u_boot 加载到了 SDRAM 的空间为:21F00000 -> 21F16B10,这恰好是 SDRAM的高端部分。另外,BSS为 21F1AE34。 (9)、编译后,可以写入 flash了。 a. 压缩这个 u-boot.bin “gzip –c u-boot.bin > u-boot.gz” 压缩后的文件大小为: 43Kbytes b. 接着把 boot.bin和 u-boot.gz 烧到 flash里面去。 Boot.bin大约 11kBytes,在 flash的 0x1000 0000 ~ 0x1000 3fff U-Boot移植过程 ① 获得发布的最新版本 U-Boot源码,与 Linux内核源码类似,也是 bzip2 的压缩格式。可 从 U-Boot的官方网站 http://sourceforge.net/projects/U-Boot上获得; ② 阅读相关文档,主要是 U-Boot 源码根目录下的 README 文档和 U-Boot 官方网站的 DULG ( The DENX U-Boot and Linux Guide ) 文档 http://www.denx.de/twiki/bin/view/DULG/Manual。尤其是DULG 文档,从如何安装建立交叉 开发环境和解决 U-Boot移植常见问题都一一给出详尽的说明; Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) ③ 订阅 U-Boot 用户邮件列表 http://lists.sourceforge.net/lists/listinfo/u-boot-users。在移植 U-Boot 过程遇有问题 , 在参考相关文档和搜 索 U-Boot-User 邮 件 档 案 库 http://sourceforge.net/mailarchive/forum.php?forum_id=12898 仍不能解决的情况下,第一时间 提交所遇到的这些问题,众多热心的 U-Boot开发人员会乐于迅速排查问题,而且很有可能, W.D本人会直接参与指导; ④ 在建立的开发环境下进行移植工作。绝大多数的开发环境是交叉开发环境。在这方面, DENX 和 MontaVista 均提供了完整的开发工具集; ⑤ 在目标板与开发主机间接入硬件调试器。 这是进行U-Boot移植应当具备且非常关键的调 试工具。因为在整个 U-Boot的移植工作,尤其是初始阶段,硬件调试器是我们了解目标板 真实运行状态的唯一途径。 在这方面, W.D 本人和众多嵌入式开发人员倾向于使用 BDI2000。 一方面,其价格不如 ICE 调试器昂贵,同时其可靠性高,功能强大,完全能胜任移植和调 试 U-Boot。另外,网上也有不少关于 BDI2000调试方面的参考文档。 ⑥ 如果在参考开发板上移植 U-Boot,可能需要移除目标板上已有的 BOOT LOADER。可以 根据板上 BOOT LOADER的说明文档,先着手解决在移除当前 BOOT LOADER的情况下, 如何进行恢复。以便今后在需要场合能重新装入原先的BOOT LOADER。 U-Boot移植方法 当前,对于 U-Boot的移植方法,大致分为两种。一种是先用 BDI2000创建目标板初始运行 环境,将 U- Boot镜像文件 u-boot.bin下载到目标板 RAM的指定位置,然后,用 BDI2000 进行跟踪调试。其好处是不用将 U-Boot 镜像文件烧写到 FLASH 去。但弊端在于对移植 开发人员的移植调试技能要求较高,BDI2000 的配置文件较为复杂。另外一种方法是用 BDI2000先将 U-Boot 镜像文件烧写到 FLASH 去,然后利用GDB和 BDI2000进行调试。 这种方法所用 BDI2000的配置文件较为简单,调试过程与 U-Boot移植后运行过程相吻合, 即 U-Boot先从 FLASH 运行,再重载至 RAM相应位置,并从那里正式投入运行。唯一 感到有些麻烦的就是需要不断烧写 FLASH。 但考虑到 FLASH 常规擦写次数基本为 10万次 左右,作为移植 U-Boot,不会占用太多的次数,应该不会为 FLASH 烧写有什么担忧。同时, W. D本人也极力推荐使用后一种方法。笔者建议,除非U-Boot移植资深人士或有强有力的 技术支持,建议采用第二种移植方法。 U-Boot移植主要修改的文件 从移植 U-Boot最小要求-U-Boot能正常启动的角度出发,主要考虑修改如下文件: ① .h头文件,如 include/configs/RPXlite.h。可以是 U-Boot源码已有的目标板头 文件,也可以是新命名的配置头文件;大多数的寄存器参数都是在这一文件设置完成的; ② .c文件, 如board/RPXlite/RPXlite.c。 它是SDRAM的驱动程序, 主要完成SDRAM Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)的 UPM表设置,上电初始化。 ③ FLASH的驱动程序, 如board/RPXlite/flash.c, 或common/cfi_flash.c。 可在参考已有FLASH 驱动的基础上,结合目标板 FLASH 数据手册,进行适当修改; ④ 串口驱动,如修改cpu/mpc8xx/serial.c串口收发器芯片使能部分。 U-Boot移植要点 ① BDI2000 的配置文件。如果采用第二种移植方法,即先烧入 FLASH 的方法,配置项只 需很少几个,就可以进行 U-Boot的烧写与调试了。对 PPC 8xx系列的主板,可参考DULG 文档 TQM8xx 的配置文件进行相应的修改。下面,笔者以美国 Embedded Planet 公司的 RPXlite DW 板为例,给出在嵌入式Linux交叉开发环境下的 BDI2000参考配置文件以作参 考: ; bdiGDB configuration file for RPXlite DW or LITE_DW ; -------------------------------------------- [INIT] ; init core register WSPR 149 0x2002000F ;DER : set debug enable register ; WSPR 149 0x2006000F ;DER : enable SYSIE for BDI flash program WSPR 638 0xFA200000 ;IMMR : internal memory at 0xFA200000 WM32 0xFA200004 0xFFFFFF89 ;SYPCR [TARGET] CPUCLOCK 40000000 ;the CPU clock rate after processing the init list BDIMODE AGENT ;the BDI working mode (LOADONLY | AGENT) BREAKMODE HARD ;SOFT or HARD, HARD uses PPC hardware breakpoints [HOST] IP 173.60.120.5 FILE uImage.litedw FORMAT BIN LOAD MANUAL ;load code MANUAL or AUTO after reset DEBUGPORT 2001 START 0x0100 [FLASH] CHIPTYPE AM29BX8 ;;Flash type (AM29F | AM29BX8 | AM29BX16 | I28BX8 | I28BX16) CHIPSIZE 0x400000 ;;The size of one flash chip in bytes BUSWIDTH 32 ;The width of the flash memory bus in bits (8 | 16 | 32) WORKSPACE 0xFA202000 ; RAM buffer for fast flash programming FILE u-boot.bin ;The file to program FORMAT BIN 0x00000000 ERASE 0x00000000 BLOCK ERASE 0x00008000 BLOCK ERASE 0x00010000 BLOCK Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)ERASE 0x00018000 BLOCK [REGS] DMM1 0xFA200000 FILE reg823.def ② U-Boot 移植参考板。这是进行 U-Boot 移植首先要明确的。可以根据目标板上 CPU、 FLASH、SDRAM的情况,以尽可能相一致为原则,先找出一个与所移植目标板为同一个或 同一系列处理器的 U-Boot 支持板为移植参考板。如 RPXlite DW 板可选择 U-Boot 源码 RPXlite 板作为 U-Boot 移植参考板。对 U-Boot 移植新手,建议依照循序渐进的原则,目标 板文件名暂时先用移植参考板的名称,在逐步熟悉 U-Boot 移植基础上,再考虑给目标板重 新命名。在实际移植过程,可用 Linux 命令查找移植参考板的特定代码,如 grep –r RPXlite ./ 可确定出在 U-Boot与 RPXlite板有关的代码,依此对照目标板实际进行屏蔽或 修改。同时应不局限于移植参考板的代码,要广泛借鉴U-Boot 已有的代码更好地实现 一些具体的功能。 ③ U-Boot烧写地址。 不同目标板, 对 U-Boot在 FLASH 存放地址要求不尽相同。 事实上, 这是由处理器断复位向量来决定的,与主板硬件相关,对 MPC8xx 主板来讲,就是由硬 件配置字(HRCW)决定的。也就是说,U-Boot烧写具体位置是由硬件决定的,而不是程序设 计来选择的。程序相应 U-Boot 起始地址必须与硬件所确定的硬件复位向量相吻合;如 RPXlite DW 板的断复位向量设置为 0x00000100。因此, U-Boot 的 BIN 镜像文件必须烧 写到 FLASH 的起始位置。 事实上, 大多数的 PPC系列的处理器断复位向量是 0x00000100 和 0xfff00100。这也是一般所说的高位启动和低位启动的 BOOT LOADER 所在位置。可通 过修改 U-Boot 源码.h 头文件 CFG_MONITOR_BASE 和 board//config.mk的 TEXT_BASE 的设置来与硬件配置相对应。 ④ CPU寄存器参数设置。根据处理器系列、类型不同,寄存器名称与作用有一定差别。必 须根据目标板的实际,进行合理配置。一个较为可行和有效的方法,就是借鉴参考移植板的 配置,再根据目标板实际,进行合理修改。这是一个较费功夫和考验耐力的过程,需要仔细 对照处理器各寄存器定义、参考设置、目标板实际作出选择并不断测试。MPC8xx处理器较 为关键的寄存器设置为 SIUMCR、PLPRCR、SCCR、BRx、ORx。 ⑤ 串口调试。能从串口输出信息,即使是乱码,也可以说 U-Boot移植取得了实质性突破。 依据笔者调试经历,串口是否有输出,除了与串口驱动相关外,还与 FLASH 相关的寄存器 设置有关。因为 U-Boot 是从 FLASH 被引导启动的,如果 FLASH 设置不正确,U-Boot 代码读取和执行就会出现一些问题。因此,还需要就FLASH 的相关寄存器设置进行一些参 数调试。同时,要注意串口收发芯片相关引脚工作波形。依据笔者调试情况,如果串口无输 出或出现乱码,一种可能就是该芯片损坏或工作不正常。 ⑥ 与启动 FLASH 相关的寄存器 BR0、OR0 的参数设置。应根据目标板 FLASH 的数据手 册与 BR0 和 OR0 的相关位含义进行合理设置。这不仅关系到 FLASH 能否正常工作,而且 与串口调试有直接的关联。 ⑦ 关于 CPLD 电路。目标板上是否有 CPLD 电路丝毫不会影响 U-Boot 的移植与嵌入式操 作系统的正常运行。事实上,CPLD 电路是一个集将板上电路的一些逻辑关系可编程设置 Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)的一种实现方法。其本身所起的作用就是实现一些目标板所需的脉冲信号和电路逻辑,其功 能完全可以用一些逻辑电路与 CPU口线来实现。 ⑧ SDRAM的驱动。串口能输出以后,U-Boot移植是否顺利基本取决于 SDRAM的驱动是 否正确。与串口调试相比,这部分工作更为核心,难度更大。 MPC8xx 目标板 SDRAM 驱 动涉及三部分。一是相关寄存器的设置;二是 UPM表;三是 SDRAM上电初始化过程。任 何一部分有问题,都会影响 U- Boot、嵌入式操作系统甚至应用程序的稳定、可靠运行。所 以说,SDRAM 的驱动不仅关系到 U-Boot 本身能否正常运行,而且还与后续部分相关,是 相当关键的部分。 ⑨ 补充功能的添加。在获得一个能工作的 U-Boot后,就可以根据目标板和实际开发需要, 添加一些其它功能支持。如以太网、LCD、NVRAM 等。与串口和 SDRAM 调试相比,在 已有基础之上,这些功能添加还是较为容易的。大多只是在参考现有源码的基础上,进行一 些修改和配置。 另外,如果在自主设计的主板上移植 U-Boot,那么除了考虑上述软件因素以外,还需要排 查目标板硬件可能存在的问题。如原理设计、PCB 布线、元件好坏。在移植过程,敏锐 判断出故障态是硬件还是软件问题,往往是关系到项目进度甚至移植成败的关键,相应难度 会增加许多。 下面以移植 u-boot 到 44B0开发板的步骤为例,移植上仅需要修改和硬件相关的部分。在 代码结构上: 1) 在 board 目录下创建 ev44b0ii 目录,创建 ev44b0ii.c 以及 flash.c,memsetup.S,u-boot.lds 等。不需要从零开始,可选择一个相似的目录,直接复制过来,修改文件名以及内容。我在 移植 u-boot 过程,选择的是 ep7312 目录。由于 u-boot 已经包含基于 s3c24b0 的开发板 目录,作为参考,也可以复制相应的目录。 2) 在 cpu 目录下创建 arm7tdmi 目录,主要包含 start.S, interrupts.c 以及 cpu.c,serial.c几个文 件。同样不需要从零开始建立文件,直接从arm720t 复制,然后修改相应内容。 3) 在 include/configs 目录下添加 ev44b0ii.h,在这里放上全局的宏定义等。 4) 找到 u-boot 根目录下 Makefile 修改加入 ev44b0ii_config : unconfig @./mkconfig $(@:_config=) arm arm7tdmi ev44b0ii 5) 运行 make ev44bii_config,如果没有错误就可以开始硬件相关代码移植的工作 u-boot 的体系结构 1) 总体结构 u-boot 是一个层次式结构。从上图也可以看出,做移植工作的软件人员应当提供串口驱动 Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)(UART Driver),以太网驱动(Ethernet Driver),Flash 驱动(Flash 驱动),USB 驱动(USB Driver)。目前,通过 USB 口下载程序显得不是十分必要,所以暂时没有移植 USB 驱动。 驱动层之上是 u-boot 的应用,command 通过串口提供人机界面。我们可以使用一些命令做 一些常用的工作,比如内存查看命令 md。 Kermit 应用主要用来支持使用串口通过超级终端下载应用程序。TFTP 则是通过网络方式 来下载应用程序,例如uclinux 操作系统。 2) 内存分布 在 flash rom 内存分布图 ev44b0ii 的 flash 大小 2M(8bits),现在将 0-40000 共 256k 作为 u-boot 的存储空间。由于 u-boot 有一些环境变量,例如 ip 地址,引导文件名等,可在命 令行通过 setenv 配置好,通过 saveenv 保存在 40000-50000(共 64k)这段空间里。如果存在 保存好的环境变量,u-boot 引导将直接使用这些环境变量。正如从代码分析可以看到, 我们会把 flash 引导代码搬移到 DRAM 运行。下图给出 u-boot 的代码在 DRAM 的位 置。引导代码 u-boot 将从 0x0000 0000 处搬移到 0x0C700000 处。特别注意的由于 ev44b0ii uclinux 断向量程序地址在 0x0c00 0000 处,所以不能将程序下载到0x0c00 0000 出,通 常下载到 0x0c08 0000 处。 2) start.S 代码结构 1) 定义入口 一个可执行的 Image 必须有一个入口点并且只能有一个唯一的全局入口,通常这个入口放 在 Rom(flash)的 0x0 地址。例如 start.S 的 .globl _start _start: 值得注意的是你必须告诉编译器知道这个入口, 这个工作主要是修改连接器脚本文件 (lds)。 2) 设置异常向量(Exception Vector) 异常向量表,也可称为断向量表,必须是从 0 地址开始,连续的存放。如下面的就包括 了复位(reset),未定义处理(undef),软件断(SWI),预去指令错误(Pabort),数据错误 (Dabort), 保留,以及 IRQ,FIQ 等。注意这里的值必须与 uclinux 的 vector_base 一致。这就是说如果 uclinux vector_base(include/armnommu/proc-armv/system.h) 定 义 为 0x0c00 0000, 则 HandleUndef 应该在 0x0c00 0004。 b reset //for debug ldr pc,=HandleUndef ldr pc,=HandleSWI ldr pc,=HandlePabort ldr pc,=HandleDabort b . ldr pc,=HandleIRQ ldr pc,=HandleFIQ ldr pc,=HandleEINT0 /*mGA H/W interrupt vector table*/ ldr pc,=HandleEINT1 ldr pc,=HandleEINT2 Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)ldr pc,=HandleEINT3 ldr pc,=HandleEINT4567 ldr pc,=HandleTICK /*mGA*/ b . b . ldr pc,=HandleZDMA0 /*mGB*/ ldr pc,=HandleZDMA1 ldr pc,=HandleBDMA0 ldr pc,=HandleBDMA1 ldr pc,=HandleWDT ldr pc,=HandleUERR01 /*mGB*/ b . b . ldr pc,=HandleTIMER0 /*mGC*/ ldr pc,=HandleTIMER1 ldr pc,=HandleTIMER2 ldr pc,=HandleTIMER3 ldr pc,=HandleTIMER4 ldr pc,=HandleTIMER5 /*mGC*/ b . b . ldr pc,=HandleURXD0 /*mGD*/ ldr pc,=HandleURXD1 ldr pc,=HandleIIC ldr pc,=HandleSIO ldr pc,=HandleUTXD0 ldr pc,=HandleUTXD1 /*mGD*/ b . b . ldr pc,=HandleRTC /*mGKA*/ b . b . b . b . b . /*mGKA*/ b . b . ldr pc,=HandleADC /*mGKB*/ b . b . b . b . b . /*mGKB*/ b . Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)b . ldr pc,=EnterPWDN 作为对照:请看以上标记的值: .equ HandleReset, 0xc000000 .equ HandleUndef,0xc000004 .equ HandleSWI, 0xc000008 .equ HandlePabort, 0xc00000c .equ HandleDabort, 0xc000010 .equ HandleReserved, 0xc000014 .equ HandleIRQ, 0xc000018 .equ HandleFIQ, 0xc00001c /*the value is different with an address you think it may be. *IntVectorTable */ .equ HandleADC, 0xc000020 .equ HandleRTC, 0xc000024 .equ HandleUTXD1, 0xc000028 .equ HandleUTXD0, 0xc00002c .equ HandleSIO, 0xc000030 .equ HandleIIC, 0xc000034 .equ HandleURXD1, 0xc000038 .equ HandleURXD0, 0xc00003c .equ HandleTIMER5, 0xc000040 .equ HandleTIMER4, 0xc000044 .equ HandleTIMER3, 0xc000048 .equ HandleTIMER2, 0xc00004c .equ HandleTIMER1, 0xc000050 .equ HandleTIMER0, 0xc000054 .equ HandleUERR01, 0xc000058 .equ HandleWDT, 0xc00005c .equ HandleBDMA1, 0xc000060 .equ HandleBDMA0, 0xc000064 .equ HandleZDMA1, 0xc000068 .equ HandleZDMA0, 0xc00006c .equ HandleTICK, 0xc000070 .equ HandleEINT4567, 0xc000074 .equ HandleEINT3, 0xc000078 .equ HandleEINT2, 0xc00007c .equ HandleEINT1, 0xc000080 .equ HandleEINT0, 0xc000084 3) 初始化 CPU 相关的 pll,clock,断控制寄存器 依次为关闭 watch dog timer,关闭断,设置 LockTime,PLL(phase lock loop),以及时钟。 Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)这些值(除了LOCKTIME)都可从 Samsung 44b0 的手册查到。 ldr r0,WTCON //watch dog disable ldr r1,=0x0 str r1,[r0] ldr r0,INTMSK ldr r1,MASKALL //all interrupt disable str r1,[r0] /***************************************************** * Set clock control registers * *****************************************************/ ldr r0,LOCKTIME ldr r1,=800 // count = t_lock * Fin (t_lock=200us, Fin=4MHz) = 800 str r1,[r0] ldr r0,PLLCON /*temporary setting of PLL*/ ldr r1,PLLCON_DAT /*Fin=10MHz,Fout=40MHz or 60MHz*/ str r1,[r0] ldr r0,CLKCON ldr r1,=0x7ff8 //All unit block CLK enable str r1,[r0] 4) 初始化内存控制器 内存控制器,主要通过设置 13 个从 1c80000 开始的寄存器来设置,包括总线宽度, 8 个内存 bank,bank 大小,sclk,以及两个 bank mode。 /***************************************************** * Set memory control registers * *****************************************************/ memsetup: adr r0,SMRDATA ldmia r0,{r1-r13} ldr r0,=0x01c80000 //BWSCON Address stmia r0,{r1-r13} 5) 将 rom 的程序复制到 RAM 首先利用 PC 取得 bootloader 在 flash 的起始地址,再通过标号之差计算出这个程序代 码的大小。这些标号,编译器会在连接(link)的时候生成正确的分布的值。取得正 确信息后,通过寄存器(r3 到 r10)做为复制的间媒介,将代码复制到 RAM 。 relocate: /* * relocate armboot to RAM */ Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)adr r0, _start /* r0 <- current position of code */ ldr r2, _armboot_start ldr r3, _armboot_end sub r2, r3, r2 /* r2 <- size of armboot */ ldr r1, _TEXT_BASE /* r1 <- destination address */ add r2, r0, r2 /* r2 baudrate) + 0.5) -1 )计算得出。这可以在手 册查到。其他的函数包括发送,接收。这个时候没有断,是通过循环等待来判断是否动 作完成。 例如,接收函数: while(!(rUTRSTAT0 & 0x1)); //Receive data read return RdURXH0(); 2. 时钟部分 实现了延时函数 udelay。 这里的 get_timer 由于没有使用断,是使用全局变量来累加的。 3. flash 部分 flash 作为内存的一部分,读肯定没有问题,关键是 flash 的写部分。 Flash 的写必须先擦除,然后再写。 unsigned long flash_init (void) { int i; u16 manId,devId; //first we init it as unknown,even if you forget assign it below,it's not a problem for (i=0; i < CFG_MAX_FLASH_BANKS; ++i){ flash_info[i].flash_id = FLASH_UNKNOWN; flash_info[i].sector_count=CFG_MAX_FLASH_SECT; } /*check manId,devId*/ _RESET(); _WR(0x555,0xaa); _WR(0x2aa,0x55); _WR(0x555,0x90); manId=_RD(0x0); _WR(0x555,0xaa); _WR(0x2aa,0x55); _WR(0x555,0x90); Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)devId=_RD(0x1); _RESET(); printf("flashn"); printf("Manufacture ID=%4x(0x0004), Device ID(0x22c4)=%4xn",manId,devId); if(manId!=0x0004 && devId!=0x22c4){ printf("flash check faliluren"); return 0; }else{ for (i=0; i = CFG_FLASH_BASE //onitor protection ON by default flash_protect(FLAG_PROTECT_SET, CFG_MONITOR_BASE, CFG_MONITOR_BASE+monitor_flash_len-1, &flash_info[0]); #endif */ flash_info[0].size =PHYS_FLASH_SIZE; return (PHYS_FLASH_SIZE); } flash_init 完成初始化部分,这里的主要目的是检验flash 的型号是否正确。 int flash_erase (flash_info_t *info, int s_first, int s_last) { volatile unsigned char *addr = (volatile unsigned char *)(info->start[0]); int flag, prot, sect, l_sect; //ulong start, now, last; u32 targetAddr; u32 targetSize; /*zyy note:It is required and can't be omitted*/ rNCACHBE0=( (0x2000000>>12)<>12); //flash area(Bank0) must be non-cachable area. rSYSCFG=rSYSCFG & (~0x8); //write buffer has to be off for proper timing. if ((s_first s_last)) { if (info->flash_id == FLASH_UNKNOWN) { printf ("- missingn"); } else { printf ("- no sectors to erasen"); Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)} return 1; } if ((info->flash_id == FLASH_UNKNOWN) || (info->flash_id > FLASH_AMD_COMP)) { printf ("Can't erase unknown flash type - abortedn"); return 1; } prot = 0; for (sect=s_first; sectprotect[sect]) { prot++; } } if (prot) { printf ("- Warning: %d protected sectors will not be erased!n", prot); } else { printf ("n"); } l_sect = -1; /* Disable interrupts which might cause a timeout here */ flag = disable_interrupts(); /* Start erase on unprotected sectors */ for (sect = s_first; sectprotect[sect] == 0) {/* not protected */ targetAddr=0x10000*sect; if(targetAddr<0x1F0000) targetSize=0x10000; else if(targetAddr<0x1F8000) targetSize=0x8000; else if(targetAddr<0x1FC000) targetSize=0x2000; else targetSize=0x4000; F29LV160_EraseSector(targetAddr); l_sect = sect; if(!BlankCheck(targetAddr, targetSize)) printf("BlankCheck Errorn"); } } /* re-enable interrupts if necessary */ if (flag) enable_interrupts(); Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)/* wait at least 80us - let's wait 1 ms */ udelay (1000); /* *We wait for the last triggered sector */ if (l_sect > 16) & 0xffff; low=swap_16(low); high=swap_16(high); tempPt=(volatile u16 *)dest; _WR(0x555,0xaa); _WR(0x2aa,0x55); _WR(0x555,0xa0); *tempPt=high; Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)_WAIT(); _WR(0x555,0xaa); _WR(0x2aa,0x55); _WR(0x555,0xa0); *(tempPt+1)=low; _WAIT(); return 0; } wirte_word 则想 flash 里面写入 unsigned long 类型的 data, 因为flash 一次只能写入16bits, 所以这里分两次写入。 Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com)u-boot源码分析——启动第一阶段 分析代码当然要从上电后执行的第一条指令开始看起咯, 那第一条指令在哪呢? 还是以 smdk2410 为 例,我们看它的链接脚本: 文件 board/smsk2410/u-boot.lds: …… ENTRY(_start) //指明入口地址(见汇编指令) SECTIONS { . = 0x00000000; //入口地址为 0x00000000,硬件决定的 . = ALIGN(4); //按 4 字节对齐,即按字对齐(32 位) .text: //文本段,即代码段 { cpu/arm920t/start.o (.text) //确定启动后执行的第一个文件 *(.text) } . = ALIGN(4); .rodata : { *(.rodata) } …… } 由这个文件可知第一个执行的文件是 cpu/arm920t/start.S,那第一条指令(_start)很可能就在这个文件 了。我们看这个文件: cpu/arm920t/start.S: .globl _start /*这 8 行为断向量表,参考arm书籍可确定这段代码的编写方法*/ _start: b reset //复位向量,CPU上电后执行的第一条语句 ldr pc, _undefined_instruction ldr pc, _software_interrupt ldr pc, _prefetch_abort ldr pc, _data_abort ldr pc, _not_used ldr pc, _irq //断向量 ldr pc, _fiq //快速断向量 /*.word为伪指令,变量替换*/ _undefined_instruction: .word undefined_instruction _software_interrupt: .word software_interrupt Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) _prefetch_abort: .word prefetch_abort _data_abort: .word data_abort _not_used: .word not_used _irq: .word irq _fiq: .word fiq S3C2410的 CPU规定开机后的 PC寄存器地址为 0,即从 0 地址开始执行指令,因此我们必须把我们的 复位代码放在 0 地址处才能正常开机。 ARM核也规定启动地址处的 32个字节必须存放异常向量跳转表,里面保存有断,异常等的处理函数 地址。当系统产生断时,必定会跳到这里来开始处理断。具体可参考 ARM方面的书籍。 由 u-boot.lds可知入口地址为_start, 即开机后从_start处开始执行指令。所以第一条指令就是: b reset //跳转到 reset处进行复位处理 cpu/arm920t/start.S: // CPU上电后跳转到此处,CPU进入 SVC32模式,这样可以拥有特权操作,参考 ARM书籍 /* the actual reset code */ reset: mrs r0,cpsr bic r0,r0,#0x1f orr r0,r0,#0xd3 msr cpsr,r0 /* turn off the watchdog */ //CPU上操作 watchdog相关的寄存器地址,可参考CPU的 datasheet,这里用到的地址都是实地址, //因为还没为 MMU等部件进行初始化,也没切换操作模式呢。 #if defined(CONFIG_S3C2400) # define pWTCON 0x15300000 # define INTMSK 0x14400008 /* Interupt-Controller base addresses */ # define CLKDIVN 0x14800014 /* clock divisor register */ #elif defined(CONFIG_S3C2410) # define pWTCON 0x53000000 # define INTMSK 0x4A000008 /* Interupt-Controller base addresses */ # define INTSUBMSK 0x4A00001C # define CLKDIVN 0x4C000014 /* clock divisor register */ #endif #if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410) ldr r0, =pWTCON mov r1, #0x0 Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) str r1, [r0] //关闭 watchdog,具体寄存器含义可参考 CPU手册 /* * mask all IRQs by setting all bits in the INTMSK - default */ mov r1, #0xffffffff ldr r0, =INTMSK str r1, [r0] //关闭所有的断 # if defined(CONFIG_S3C2410) ldr r1, =0x3ff ldr r0, =INTSUBMSK str r1, [r0] //关闭所有的断 # endif /* FCLK:HCLK:PCLK = 1:2:4 */ /* default FCLK is 120 MHz ! */ //设置 HCLK 为 FCLK/2, PCLK 为 FCLK/4, FCLK 为 CPU产生 clock,HCLK 为 AHB总线上的设备产生 //clock, PCLK 为 APB总线上的设备产生 clock,具体参考 s3c2410的 datasheet ldr r0, =CLKDIVN mov r1, #3 str r1, [r0] #endif /* CONFIG_S3C2400 || CONFIG_S3C2410 */ /* * we do sys-critical inits only at reboot, * not when booting from ram! */ //做系统相关的重要初始化,这些初始化代码只在系统重起的时候执行, // CONFIG_SKIP_LOWLEVEL_INIT 可以看 README. #ifndef CONFIG_SKIP_LOWLEVEL_INIT bl cpu_init_crit //可以先看这段代码在转回来接着看后面的复位过程。 #endif //内存配置完后,可以进行重定位操作了 #ifndef CONFIG_SKIP_RELOCATE_UBOOT relocate: /* 重定位 u-boot到 RAM*/ adr r0, _start /* r0 = flash的代码的起始地址*/ ldr r1, _TEXT_BASE /* r1= 代码在 RAM的起始地址 */ cmp r0, r1 /* 看是否 u-boot就在 RAM运行*/ beq stack_setup /*如果在 RAM则无需重定位*/ /*开始重定位,即把u-boot从 flash搬到 RAM 去运行*/ ldr r2, _armboot_start /*r2 = flash代码的起始地址,看_armboot_start的定义*/ ldr r3, _bss_start /*r3 = bss段的起始地址,_bss_start可在 u-boot.lds查看。*/ Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) sub r2, r3, r2 /* r2 = 需要重定位的字节数*/ add r2, r0, r2 /* r2 = flash RO,RW 内容的结束地址 */ //开始把代码从 flash搬运到 RAM copy_loop: ldmia r0!, {r3-r10} /*获取从 r0开始的代码,存入 r3—r10*/ stmia r1!, {r3-r10} /*把 r3—r10 的内容存入r1 所在位置,即 RAM*/ cmp r0, r2 /*copy所有代码 */ ble copy_loop #endif /* CONFIG_SKIP_RELOCATE_UBOOT */ /*设置栈地址*/ stack_setup: ldr r0, _TEXT_BASE /*upper 128 KiB: relocated uboot*/ sub r0, r0, #CFG_MALLOC_LEN /*malloc分配内存的区域,大小以板子的配置而定,smdk2410的在 include/configs/smdk2410.h定义*/ sub r0, r0, #CFG_GBL_DATA_SIZE /* 存放 bdinfo的区域,定义同上*/ #ifdef CONFIG_USE_IRQ sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ) //保留断所需的区域 #endif sub sp, r0, #12 /* 保留 12 字节给 abort-stack, 并设好堆栈*/ //bss段内容清 0 clear_bss: ldr r0, _bss_start /* find start of bss segment */ ldr r1, _bss_end /* stop here */ mov r2, #0x00000000 /* clear */ clbss_l:str r2, [r0] /* clear loop... */ add r0, r0, #4 cmp r0, r1 ble clbss_l #if 0 /* try doing this stuff after the relocation */ ldr r0, =pWTCON mov r1, #0x0 str r1, [r0] /* mask all IRQs by setting all bits in the INTMR - default*/ mov r1, #0xffffffff ldr r0, =INTMR str r1, [r0] /* FCLK:HCLK:PCLK = 1:2:4 */ /* default FCLK is 120 MHz ! */ ldr r0, =CLKDIVN mov r1, #3 Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) str r1, [r0] /* END stuff after relocation */ #endif ldr pc, _start_armboot //跳转到_start_armboot处执行。 _start_armboot: .word start_armboot 总结 reset这块代码,主要完成了一下几个部分: 1. 重要部分的初始化工作,如禁止断,关闭 watchdog,初始化 memory控制器等 2. 重定位boot loader 到 ram 3. 设置好堆栈 4. 跳转到第 2阶段执行 完成这些后,此时内存的分布情况如下: 这个图代表的是 u-boot自己在内存的情况, 和上面的图不一样, 这里的_TEXT_BASE 就是 0x33F8’ 0000 接着看 CPU_init_critical cpu/arm920t/start.S: /* ************************************************************************** * CPU_init_critical registers * 设置 cache,TLB,MMU等寄存器 * 设置内存操作的时序 * ************************************************************************* */ cpu_init_crit: /* * flush v4 I/D caches */ /*使 cache和 TLB无效,可以参考 data sheet*/ mov r0, #0 mcr p15, 0, r0, c7, c7, 0 /* 使指令 cache和数据 cache无效 */ mcr p15, 0, r0, c8, c7, 0 /* 使 TLB无效 */ /* * disable MMU stuff and caches */ mrc p15, 0, r0, c1, c0, 0 /*读出 c1 控制寄存器的值*/ bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS) bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM),小端对齐,关闭数据 cache,关 //闭错误检测,关闭MMU orr r0, r0, #0x00000002 @ set bit 2 (A) Align, 使能错误检测 orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache, 使能指令 cache mcr p15, 0, r0, c1, c0, 0 /*设置 c1 控制寄存器*/ /*可以参考 data sheet*/ Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) /* * before relocating, we have to setup RAM timing * because memory timing is board-dependend, you will * find a lowlevel_init.S in your board directory. */ //在把 u-boot 重定位到 RAM 前,我们必须先把 RAM 的时序设置好,内存时序是依板子而定的, 所以这里的初始化应该由我们提供,一般在我们的板子所在目录下有个 lowlevel_init.S来负责这件事情。 特定板子的目录还记得吗, 呵呵回到上面在看看。 mov ip, lr bl lowlevel_init mov lr, ip mov pc, lr //类似于函数返回 cpu_init_crit主要是使能了 instruction cache,关闭了 MMU等部件,但是好像在 u-boot后面的代码里没有 看见打开 MMU 的操作,我猜测可能是留到了 OS 启动的时候再打开了吧,data cache 在第二阶段的 board_init下被使能。 接着看 lowlevel_init。以 smdk2410位例 board/smdk2410/lowlevel_init.S _TEXT_BASE: .word TEXT_BASE .globl lowlevel_init lowlevel_init: /* memory control configuration */ /* make r0 relative the current location so that it */ /* reads SMRDATA out of FLASH rather than memory ! */ // 内存控制器的配置, 配置完后就可以使用内存了 ldr r0, =SMRDATA //在下面定义 ldr r1, _TEXT_BASE sub r0, r0, r1 // ldr r1, =BWSCON /* Bus Width Status Controller */ add r2, r0, #13*4 0: ldr r3, [r0], #4 str r3, [r1], #4 //设置内存配置寄存器,可以对着datasheet来看这里的设置,包括时序位宽等 等, 使用一个循环来配置所有的寄存器 cmp r2, r0 bne 0b /* everything is fine now */ mov pc, lr .ltorg /* the literal pools origin */ //这些就是要被设置进内存配置寄存器的值, SMRDATA: Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) .word (0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON< <20)+(B6_BWSCON<<24)+(B7_BWSCON<<28)) .word ((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC)) .word ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC)) .word ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC)) .word ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC)) .word ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC)) .word ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC)) .word ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN)) .word ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN)) .word ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT) .word 0x32 .word 0x30 .word 0x30 这部分代码主要是设置 memory的时序,位宽等参数 Create PDF files without this message by purchasing novaPDF printer (http://www.novapdf.com) U-BOOT源码分析及移植 本文从以下几个方面粗浅地分析 u-boot并移植到 FS2410 板上: 1、u-boot工程的总体结构 2、u-boot的流程、主要的数据结构、内存分配。 3、u-boot的重要细节,主要分析流程函数的功能。 4、基于 FS2410板子的u-boot移植。实现了 NOR Flash和 NAND Flash启动,网络功能。 这些认识源于自己移植 u-boot过程查找的资料和对源码的简单阅读。下面主要以 smdk2410为分析对 象。 一、u-boot工程的总体结构: 1、源代码组织 对于 ARM而言,主要的目录如下: board 平台依赖 存放电路板相关的目录文件,每一套板子对 应一个目 录。如 smdk2410(arm920t) cpu 平台依赖 存放 CPU 相关的目录文件,每一款 CPU 对应一个目 录,例如:arm920t、 xscale、i386 等目录 lib_arm 平台依赖 存放对 ARM 体系结构通用的文件,主要用于实现 ARM平台通用的函数,如软件浮点。 common 通用 通用的多功能函数实现,如环境,命令,控制台相关的函数实 现。 include 通用 头文件和开发板配置文件,所有开发板的配置文件都在 configs目录下 lib_generic 通用 通用库函数的实现 net 通用 存放网络协议的程序 drivers 通用 通用的设备驱动程序,主要有以太网接口的驱动,nand 驱 动。 ....... 2.makefile简要分析 所有这些目录的编译连接都是由顶层目录的 makefile 来确定的。 在执行 make之前,先
C语言各种常用关键字详细解释学习,都是总结面试,都是比较好的笔试题。 1 变量 3 1.1 全局变量,局部变量,,常量分别保存在内存的什么地方? 3 1.2 不同类型的变量,你是否知道其作用域? 3 1.3 全局变量和局部变量是否可以重名?你是否在这样做? 3 1.4 局部变量在函数退出后是否有效,为什么? 3 1.5 全局变量为什么不允许定义在头文件?有何危害? 3 2 Static 4 2.1 static的三个主要作用是什么? 4 2.2 static的修饰的局部变量是保存在什么地方的?全局变量哪? 5 2.3 static修饰的全局变量和函数,在其他的文件是否可以访问?如何访问? 5 2.4 你知道static是c语言实现封装和隐藏的利器吗?你是否经常使用? 5 2.5 定义在不同源文件static全局变量,编译器是否允许他们的变量名称相同? 5 2.6 你是否知道auto、extern、register、volatile、restrict、inline的用法? 6 3 const 6 3.1 你是否经常使用const来表明不能够被更改的变量? 6 3.2 你是否经常使用const常量来代替宏? 6 3.3 下面四种情况,你知道是各表示什么意思吗? 6 3.4 你知道const常量如何初始化吗? 7 4 enum枚举 7 4.1 到底enum所定义出来的类型是一个什么样的类型呢? 7 4.2 作为一个用户自定义的类型其所占用的内存空间是多少呢? 8 4.3 是否可以指定枚举各项的值? 8 4.4 如果不指定值,枚举的第一个值是多少? 8 4.5 枚举的值是否可以是负数? 8 4.6 定义枚举的时候,你是否专门定义了枚举的最小值和最大值? 9 5 switch 9 5.1 switch(c)的c的数据类型有哪些? 9 5.2 你是否在所有的switch都加了default语句? 9 5.3 是否在所有的case都加了break语句(一般情况的做法)?如果你不加break,将会发生什么? 9 6 sizeof 9 6.1 对于字符数组,strlen和sizeof的值是否相同? 9 6.2 Sizeof本质上是什么?函数?宏?其他? 9 6.3 Sizeof的返回值是什么类型? 10 7 动态分配内存 10 7.1 动态分配的内存是保存在什么地方的? 10 7.2 什么情况下使用动态分配内存? 10 7.3 动态申请内存一定要释放,否则会内存泄露。你是否使用过内存检测工具? 10 8 字符串 10 8.1 strlen()函数的返回值是什么类型的? 10 8.2 字符串strlen() 的值,是否和他占据的内存空间相同? 10 8.3 你是否知道strcpy 函数存在的潜在风险?如何避免? 11 8.4 如果一个字符串没有字符串结束符,而调用str 开头的库函数,会发生什么? ?? 11 8.5 Strcpy(),strcat(),strcmp(),strncpy(),strncat(),strncmp()内部到底是如何运行的?这些函数到底对源字符串和目标字符串做了些什么?你是否观察过它们运行时两个字符串内存的变化? 11 8.6 上面这些函数使用时,各有哪些需要注意的地方? 11 8.7 你会几种字符串查找操作? 11 8.8 C语言有字符串这个数据类型吗? 11 8.9 对字符串进行操作的时候,是否为字符串结尾符预留存储位置?不然的话容易造成非常访问内存。 11 9 数组 12 9.1 你肯定知道,定义“int a[10]; ”, a[10] 这个元素是无效的。 12 9.2 你知道几种数组初始化的方法? 12 9.3 数组和指针有千丝万缕的联系而又不同,你是否对他们在不同情况下的使用进行过详细的总结? 12 9.4 “int calendar[10][20]; ”,这是一个什么样的数组?它拥有 10 数组类型的元素,还是 20 个? 12 9.5 “int a[10]; ”,数组名 a 在本质上是一个什么?你是否打印过 a 的值? 12 9.6 你知道几种获取数组某元素的方法? 12 9.7 指针和数组相同吗?什么时候相同?什么时候不同? 12 9.8 用指针和下标访问数组元素,那种方式更快? 16 10 结构体 16 10.1 你知道什么是位域结构体吗?如何定义它?如何使用它? 16 10.2 你知道字节对齐对结构体占用内存空间大小的影响吗?如何计算结构体占用内存的小? 17 11 宏 19 11.1 你知道宏的本质是什么吗?函数?语句?类型定义?或者其他? 19 11.2 你知道语言设计者为什么设计宏吗?这些原因目前是否仍然成立? 19 11.3 你会设计带有参数的宏吗? 19 11.4 你知道使用宏的参数的的时候的注意事项吗? 19 11.5 你会设计带有可变参数的宏吗? 19 11.6 你知道使用宏有什么劣势吗? 20 11.7 你有没有更好的替代方案? 20 11.8 为什么说宏定义不好? 21 12 如何避免对同一头文件的多次include? 22 13 什么是声明,什么是定义? 22 14 你是否会运用c语言声明的优先级规则? 23 15 什么是预编译,何时需要预编译? 23 Total time: 90 minutes 23 16 指针数组和数组指针的区别? 30 16.1 指针的声明 30 16.1.1 一维数组 31 16.1.2 多维数组 32 16.1.3 指针数组 33 16.1.4 指针的指针 33
主体:(一) 一、C++概述 (一) 发展历史 1980年,Bjarne Stroustrup博士开始着手创建一种模拟语言,能够具有面向对象的程序设计特色。在当时,面向对象编程还是一个比较新的理念,Stroustrup博士并不是从头开始设计新语言,而是在C语言的基础上进行创建。这就是C++语言。 1985年,C++开始在外面慢慢流行。经过多年的发展,C++已经有了多个版本。为次,ANSI和ISO的联合委员会于1989年着手为C++制定标准。1994年2月,该委员会出版了第一份非正式草案,1998年正式推出了C++的国际标准。 (二) C和C++ C++是C的超集,也可以说C是C++的子集,因为C先出现。按常理说,C++编译器能够编译任何C程序,但是C和C++还是有一些小差别。 例如C++增加了C不具有的关键字。这些关键字能作为函数和变量的标识符在C程序使用,尽管C++包含了所有的C,但显然没有任何C++编译器能编译这样的C程序。 C程序员可以省略函数原型,而C++不可以,一个不带参数的C函数原型必须把void写出来。而C++可以使用空参数列表。 C++new和delete是对内存分配的运算符,取代了C的malloc和free。 标准C++的字符串类取代了C标准C函数头文件的字符数组处理函数。 C++用来做控制态输入输出的iostream类库替代了标准C的stdio函数库。 C++的try/catch/throw异常处理机制取代了标准C的setjmp()和longjmp()函数。 二、关键字和变量 C++相对与C增加了一些关键字,如下: typename bool dynamic_cast mutable namespace static_cast using catch explicit new virtual operator false private template volatile const protected this wchar_t const_cast public throw friend true reinterpret_cast try bitor xor_e and_eq compl or_eq not_eq bitand 在C++还增加了bool型变量和wchar_t型变量: 布尔型变量是有两种逻辑状态的变量,它包含两个值:真和假。如果在表达式使用了布尔型变量,那么将根据变量值的真假而赋予整型值1或0。要把一个整型变量转换成布尔型变量,如果整型值为0,则其布尔型值为假;反之如果整型值为非0,则其布尔型值为真。布儿型变量在运行时通常用做标志,比如进行逻辑测试以改变程序流程。 #include iostream.h int main() { bool flag; flag=true; if(flag) cout<<true<<endl; return 0; } C++还包括wchar_t数据类型,wchar_t也是字符类型,但是是那些宽度超过8位的数据类型。许多外文字符集所含的数目超过256个,char字符类型无法完全囊括。wchar_t数据类型一般为16位。 标准C++的iostream类库包括了可以支持宽字符的类和对象。用wout替代cout即可。 #include iostream.h int main() { wchar_t wc; wc='b'; wout<<wc; wc='y'; wout<<wc; wc='e'; wout<<wc<<endl; return 0; } 说明一下:某些编译器无法编译该程序(不支持该数据类型)。 三、强制类型转换 有时候,根据表达式的需要,某个数据需要被当成另外的数据类型来处理,这时,就需要强制编译器把变量或常数由声明时的类型转换成需要的类型。为此,就要使用强制类型转换说明,格式如下: int* iptr=(int*) &table; 表达式的前缀(int*)就是传统C风格的强制类型转换说明(typecast),又可称为强制转换说明(cast)。强制转换说明告诉编译器把表达式转换成指定的类型。有些情况下强制转换是禁用的,例如不能把一个结构类型转换成其他任何类型。数字类型和数字类型、指针和指针之间可以相互转换。当然,数字类型和指针类型也可以相互转换,但通常认为这样做是不安全而且也是没必要的。强制类型转换可以避免编译器的警告。 long int el=123; short i=(int) el; float m=34.56; int i=(int) m; 上面两个都是C风格的强制类型转换,C++还增加了一种转换方式,比较一下上面和下面这个书写方式的不同: long int el=123; short i=int (el); float m=34.56; int i=int (m); 使用强制类型转换的最大好处就是:禁止编译器对你故意去做的事发出警告。但是,利用强制类型转换说明使得编译器的类型检查机制失效,这不是明智的选择。通常,是不提倡进行强制类型转换的。除非不可避免,如要调用malloc()函数时要用的void型指针转换成指定类型指针。 四、标准输入输出流 在C语言,输入输出是使用语句scanf()和printf()来实现的,而C++是使用类来实现的。 #include iostream.h main() //C++main()函数默认为int型,而C语言默认为void型。 { int a; cout<>a; /*输入一个数值*/ cout<<a<<endl; //输出并回车换行 return 0; } cin,cout,endl对象,他们本身并不是C++语言的组成部分。虽然他们已经是ANSI标准C++被定义,但是他们不是语言的内在组成部分。在C++不提供内在的输入输出运算符,这与其他语言是不同的。输入和输出是通过C++类来实现的,cin和cout是这些类的实例,他们是在C++语言的外部实现。 在C++语言,有了一种新的注释方法,就是‘//’,在该行//后的所有说明都被编译器认为是注释,这种注释不能换行。C++仍然保留了传统C语言的注释风格/*……*/。 C++也可采用格式化输出的方法: #include iostream.h int main() { int a; cout<>a; cout<<dec<<a<<' ' //输出十进制数 <<oct<<a<<' ' //输出八进制数 <<hex<<a<<endl; //输出十六进制数 return 0; } 从上面也可以看出,dec,oct,hex也不可作为变量的标识符在程序出现。 五、函数参数问题 (一) 无名的函数形参 声明函数时可以包含一个或多个用不到的形式参数。这种情况多出现在用一个通用的函数指针调用多个函数的场合,其有些函数不需要函数指针声明的所有参数。看下面的例子: int fun(int x,int y) { return x*2; } 尽管这样的用法是正确的,但大多数C和C++的编译器都会给出一个警告,说参数y在程序没有被用到。为了避免这样的警告,C++允许声明一个无名形参,以告诉编译器存在该参数,且调用者需要为其传递一个实际参数,但是函数不会用到这个参数。下面给出使用了无名参数的C++函数代码: int fun(int x,int) //注意不同点 { return x*2; } (二) 函数的默认参数 C++函数的原型可以声明一个或多个带有默认值的参数。如果调用函数时,省略了相应的实际参数,那么编译器就会把默认值作为实际参数。可以这样来声明具有默认参数的C++函数原型: #include iostream.h void show(int=1,float=2.3,long=6); int main() { show(); show(2); show(4,5.6); show(8,12.34,50L); return 0; } void show(int first,float second,long third) { cout<<first=<<first <<second=<<second <<third=<<third<<endl; } 上面例子,第一次调用show()函数时,让编译器自动提供函数原型指定的所有默认参数,第二次调用提供了第一个参数,而让编译器提供剩下的两个,第三次调用则提供了前面两个参数,编译器只需提供最后一个,最后一个调用则给出了所有三个参数,没有用到默认参数。 六、函数重载 在C++,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言是不允许的。 1.参数个数不同 #include iostream.h void a(int,int); void a(int); int main() { a(5); a(6,7); return 0; } void a(int i) { cout<<i<<endl; //输出5 } void a(int i,int j) { cout<<i<<j<<endl; //输出67 } 2.参数格式不同 #include iostream.h void a(int,int); void a(int,float); int main() { a(5,6); a(6,7.0); return 0; } void a(int i,int j) { cout<<i<<j<<endl; //输出56 } void a(int i,float j) { cout<<i<<j<>a; for(int i=1;i<=10;i++) //C语言,不允许在这里定义变量 { static int a=0; //C语言,同一函数块,不允许有同名变量 a+=i; cout<<::a<< <<a<<endl; } return 0; } 八、new和delete运算符 在C++语言,仍然支持malloc()和free()来分配和释放内存,同时增加了new和delete来管理内存。 1.为固定大小的数组分配内存 #include iostream.h int main() { int *birthday=new int[3]; birthday[0]=6; birthday[1]=24; birthday[2]=1940; cout<<I was born on <<birthday[0]<<'/'<<birthday[1]<<'/'<<birthday[2]<>size; int *array=new int[size]; for(int i=0;i<size;i++) array[i]=rand(); for(i=0;i<size;i++) cout<<'\n'<<array[i]; delete [] array; return 0; } 九、引用型变量 在C++,引用是一个经常使用的概念。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。 1.引用是一个别名 C++的引用是其他变量的别名。声明一个引用型变量,需要给他一个初始化值,在变量的生存周期内,该值不会改变。& 运算符定义了一个引用型变量: int a; int& b=a; 先声明一个名为a的变量,它还有一个别名b。我们可以认为是一个人,有一个真名,一个外号,以后不管是喊他a还是b,都是叫他这个人。同样,作为变量,以后对这两个标识符操作都会产生相同的效果。 #include iostream.h int main() { int a=123; int& b=a; cout<<a<<','<<b<<endl; //输出123,123 a++; cout<<a<<','<<b<<endl; //输出124,124 b++; cout<<a<<','<<b<等运算符 #include iostream.h void func1(s p); void func2(s& p); struct s { int n; char text[10]; }; int main() { static s str={123,China}; func1(str); func2(str); return 0; } void func1(s p) { cout<<p.n<<endl; cout<<p.text<<endl; } void func2(s& p) { cout<<p.n<<endl; cout<<p.text<<endl; } 从表面上看,这两个函数没有明显区别,不过他们所花的时间却有很大差异,func2()函数所用的时间开销会比func2()函数少很多。它们还有一个差别,如果程序递归func1(),随着递归的深入,会因为栈的耗尽而崩溃,但func2()没有这样的担忧。 4.以引用方式调用 当函数把引用作为参数传递给另一个函数时,被调用函数将直接对参数在调用者的拷贝进行操作,而不是产生一个局部的拷贝(传递变量本身是这样的)。这就称为以引用方式调用。把参数的值传递到被调用函数内部的拷贝则称为以传值方式调用。 #include iostream.h void display(const Date&,const char*); void swapper(Date&,Date&); struct Date { int month,day,year; }; int main() { static Date now={2,23,90}; static Date then={9,10,60}; display(now,Now: ); display(then,Then: ); swapper(now,then); display(now,Now: ); display(then,Then: ); return 0; } void swapper(Date& dt1,Date& dt2) { Date save; save=dt1; dt1=dt2; dt2=save; } void display(const Date& dt,const char *s) { cout<<s; cout<<dt.month<<'/'<<dt.day<<'/'<<dt.year<<endl; } 5.以引用作为返回值 #include iostream.h struct Date { int month,day,year; }; Date birthdays[]= { {12,12,60}; {10,25,85}; {5,20,73}; }; const Date& getdate(int n) { return birthdays[n-1]; } int main() { int dt=1; while(dt!=0) { cout<<Enter date # (1-3,0 to quit)<>dt; if(dt>0 && dt<4) { const Date& bd=getdate(dt); cout<<bd.month<<'/'<<bd.day<<'/'<<bd.year<<endl; } } return 0; } 程序都很简单,就不讲解了。 主体:(二)类的设计,构造函数和析构函数 类是编程人员表达自定义数据类型的C++机制。它和C语言的结构类似,C++类支持数据抽象和面向对象的程序设计,从某种意义上说,也就是数据类型的设计和实现。 一、类的设计 1.类的声明 class 类名 { private: //私有 ... public: //公有 ... }; 2.类的成员 一般在C++类,所有定义的变量和函数都是类的成员。如果是变量,我们就叫它数据成员如果是函数,我们就叫它成员函数。 3.类成员的可见性 private和public访问控制符决定了成员的可见性。由一个访问控制符设定的可访问状态将一直持续到下一个访问控制符出现,或者类声明的结束。私有成员仅能被同一个类的成员函数访问,公有成员既可以被同一类的成员函数访问,也可以被其他已经实例化的类函数访问。当然,这也有例外的情况,这是以后要讨论的友元函数。 类默认的数据类型是private,结构的默认类型是public。一般情况下,变量都作为私有成员出现,函数都作为公有成员出现。 类还有一种访问控制符protected,叫保护成员,以后再说明。 4.初始化 在声明一个类的对象时,可以用圆括号()包含一个初始化表。 看下面一个例子: #include iostream.h class Box { private: int height,width,depth; //3个私有数据成员 public: Box(int,int,int); ~Box(); int volume(); //成员函数 }; Box::Box(int ht,int wd,int dp) { height=ht; width=wd; depth=dp; } Box::~Box() { //nothing } int Box::volume() { return height*width*depth; } int main() { Box thisbox(3,4,5); //声明一个类对象并初始化 cout<<thisbox.volume()<<endl; return 0; } 当一个类没有private成员和protected成员时,也没有虚函数,并且不是从其他类派生出来的,可以用{}来初始化。(以后再讲解) 5.内联函数 内联函数和普通函数的区别是:内联函数是在编译过程展开的。通常内联函数必须简短。定义类的内联函数有两种方法:一种和C语言一样,在定义函数时使用关键字inline。如: inline int Box::volume() { return height*width*depth; } 还有一种方法就是直接在类声明的内部定义函数体,而不是仅仅给出一个函数原型。我们把上面的函数简化一下: #include iostream.h class Box { private: int height,width,depth; public: Box(int ht,int wd,int dp) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //声明一个类对象并初始化 cout<<thisbox.volume()<<endl; return 0; } 这样,两个函数都默认为内联函数了。 二、构造函数 什么是构造函数?通俗的讲,在类函数名和类名相同的函数称为构造函数。上面的Box()函数就是构造函数。C++允许同名函数,也就允许在一个类有多个构造函数。如果一个都没有,编译器将为该类产生一个默认的构造函数,这个构造函数可能会完成一些工作,也可能什么都不做。 绝对不能指定构造函数的类型,即使是void型都不可以。实际上构造函数默认为void型。 当一个类的对象进入作用域时,系统会为其数据成员分配足够的内存,但是系统不一定将其初始化。和内部数据类型对象一样,外部对象的数据成员总是初始化为0。局部对象不会被初始化。构造函数就是被用来进行初始化工作的。当自动类型的类对象离开其作用域时,所站用的内存将释放回系统。 看上面的例子,构造函数Box()函数接受三个整型擦黑素,并把他们赋值给立方体对象的数据成员。 如果构造函数没有参数,那么声明对象时也不需要括号。 1.使用默认参数的构造函数 当在声明类对象时,如果没有指定参数,则使用默认参数来初始化对象。 #include iostream.h class Box { private: int height,width,depth; public: Box(int ht=2,int wd=3,int dp=4) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //初始化 Box defaulbox; //使用默认参数 cout<<thisbox.volume()<<endl; //输出60 cout<<defaulbox.volume()<<endl; //输出24 return 0; } 2.默认构造函数 没有参数或者参数都是默认值的构造函数称为默认构造函数。如果你不提供构造函数,编译器会自动产生一个公共的默认构造函数,这个构造函数什么都不做。如果至少提供一个构造函数,则编译器就不会产生默认构造函数。 3.重载构造函数 一个类可以有多个构造函数。这些构造函数必须具有不同的参数表。在一个类需要接受不同初始化值时,就需要编写多个构造函数,但有时候只需要一个不带初始值的空的Box对象。 #include iostream.h class Box { private: int height,width,depth; public: Box() { //nothing } Box(int ht=2,int wd=3,int dp=4) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //初始化 Box otherbox; otherbox=thisbox; cout<<otherbox.volume();<<endl; return 0; } 这两个构造函数一个没有初始化值,一个有。当没有初始化值时,程序使用默认值,即2,3,4。 但是这样的程序是不好的。它允许使用初始化过的和没有初始化过的Box对象,但它没有考虑当thisbox给otherbox赋值失败后,volume()该返回什么。较好的方法是,没有参数表的构造函数也把默认值赋值给对象。 class Box { int height,width,depth; public: Box() { height=0;width=0;depth=0; } Box(int ht,int wd,int dp) { height=ht;width=wd;depth=dp; } int volume() { return height*width*depth; } }; 这还不是最好的方法,更好的方法是使用默认参数,根本不需要不带参数的构造函数。 class Box { int height,width,depth; public: Box(int ht=0,int wd=0,int dp=0) { height=ht;width=wd;depth=dp; } int volume() { return height*width*depth; } }; 三、析构函数 当一个类的对象离开作用域时,析构函数将被调用(系统自动调用)。析构函数的名字和类名一样,不过要在前面加上 ~ 。对一个类来说,只能允许一个析构函数,析构函数不能有参数,并且也没有返回值。析构函数的作用是完成一个清理工作,如释放从堆分配的内存。 我们也可以只给出析构函数的形式,而不给出起具体函数体,其效果是一样的,如上面的例子。但在有些情况下,析构函数又是必需的。如在类从堆分配了内存,则必须在析构函数释放 主体:(三)类的转换 C++的内部数据类型遵循隐式类型转换规则。假设某个表达市使用了一个短整型变量,而编译器根据上下文认为这儿需要是的长整型,则编译器就会根据类型转换规则自动把它转换成长整型,这种隐式转换出现在赋值、参数传递、返回值、初始化和表达式。我们也可以为类提供相应的转换规则。 对一个类建立隐式转换规则需要构造一个转换函数,该函数作为类的成员,可以把该类的对象和其他数据类型的对象进行相互转换。声明了转换函数,就告诉了编译器,当根据句法判定需要类型转换时,就调用函数。 有两种转换函数。一种是转换构造函数;另一种是成员转换函数。需要采用哪种转换函数取决于转换的方向。 一、转换构造函数 当一个构造函数仅有一个参数,且该参数是不同于该类的一个数据类型,这样的构造函数就叫转换构造函数。转换构造函数把别的数据类型的对象转换为该类的一个对象。和其他构造函数一样,如果声明类的对象的初始化表同转换构造函数的参数表相匹配,该函数就会被调用。当在需要使用该类的地方使用了别的数据类型,便宜器就会调用转换构造函数进行转换。 #include iostream.h #include time.h #include stdio.h class Date { int mo, da, yr; public: Date(time_t); void display(); }; void Date::display() { char year[5]; if(yr<10) sprintf(year,0%d,yr); else sprintf(year,%d,yr); cout<<mo<<'/'<<da<<'/'<tm_mday; mo=tim->tm_mon+1; yr=tim->tm_year; if(yr>=100) yr-=100; } int main() { time_t now=time(0); Date dt(now); dt.display(); return 0; } 本程序先调用time()函数来获取当前时间,并把它赋给time_t对象;然后程序通过调用Date类的转换构造函数来创建一个Date对象,该对象由time_t对象转换而来。time_t对象先传递给localtime()函数,然后返回一个指向tm结构(time.h文件声明)的指针,然后构造函数把结构的日月年的数值拷贝给Date对象的数据成员,这就完成了从time_t对象到Date对象的转换。 二、成员转换函数 成员转换函数把该类的对象转换为其他数据类型的对象。在成员转换函数的声明要用到关键字operator。这样声明一个成员转换函数: operator aaa(); 在这个例子,aaa就是要转换成的数据类型的说明符。这里的类型说明符可以是任何合法的C++类型,包括其他的类。如下来定义成员转换函数; Classname::operator aaa() 类名标识符是声明了该函数的类的类型说明符。上面定义的Date类并不能把该类的对象转换回time_t型变量,但可以把它转换成一个长整型值,计算从2000年1月1日到现在的天数。 #include iostream.h class Date { int mo,da,yr; public: Date(int m,int d,int y) {mo=m; da=d; yr=y;} operator int(); //声明 }; Date::operator int() //定义 { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; int days=yr-2000; days*=365; days+=(yr-2000)/4; for(int i=0;i<mo-1;i++) days+=dys[i]; days+=da; return days; } int main() { Date now(12,24,2003); int since=now; cout<<since<<endl; return 0; } 三、类的转换 上面两个例子都是C++类对象和内部数据对象之间的相互转换。也可以定义转换函数来实现两个类对象之间的相互转换。 #include iostream.h class CustomDate { public: int da, yr; CustomDate(int d=0,int y=0) {da=d; yr=y;} void display() { cout<<yr<<'-'<<da<<endl; } }; class Date { int mo, da, yr; public: Date(int m=0,int d=0,int y=0) {mo=m; da=d; yr=y;} Date(const CustomDate&); //转换构造函数 operator CustomDate(); //成员转换函数 void display() { cout<<mo<<'/'<<da<<'/'<<yr<<endl; } }; static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; Date::Date(const CustomDate& jd) { yr=jd.yr; da=jd.da; for(mo=0;modys[mo]) da-=dys[mo]; else break; mo++; } Date::operator CustomDate() { CustomDate cd(0,yr); for(int i=0;i<mo-1;i++) cd.da+=dys[i]; cd.da+=da; return cd; } int main() { Date dt(12,24,3); CustomDate cd; cd = dt; //调用成员转换函数 cd.display(); dt = cd; //调用转换构造函数 dt.display(); return 0; } 这个例子有两个类CustomDate和Date,CustomDate型日期包含年份和天数。 这个例子没有考虑闰年情况。但是在实际构造一个类时,应该考虑到所有问题的可能性。 在Date里具有两种转换函数,这样,当需要从Date型变为CustomDate型十,可以调用成员转换函数;反之可以调用转换构造函数。 不能既在Date类定义成员转换函数,又在CustomDate类里定义转换构造函数。那样编译器在进行转换时就不知道该调用哪一个函数,从而出错。 四、转换函数的调用 C++里调用转换函数有三种形式:第一种是隐式转换,例如编译器需要一个Date对象,而程序提供的是CustomDate对象,编译器会自动调用合适的转换函数。另外两种都是需要在程序代码明确给出的显式转换。C++强制类型转换是一种,还有一种是显式调用转换构造函数和成员转换函数。下面的程序给出了三转换形式: #include iostream.h class CustomDate { public: int da, yr; CustomDate(int d=0,int y=0) {da=d; yr=y;} void display() { cout<<yr<<'-'<<da<<endl; } }; class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); for(int i=0;i<mo-1;i++) cd.da+=dys[i]; cd.da+=da; return cd; } int main() { Date dt(11,17,89); CustomDate cd; cd = dt; cd.display(); cd = (CustomDate) dt; cd.display(); cd = CustomDate(dt); cd.display(); return 0; } 五、转换发生的情形 上面的几个例子都是通过不能类型对象之间的相互赋值来调用转换函数,还有几种调用的可能: 参数传递 初始化 返回值 表达式语句 这些情况下,都有可能调用转换函数。 下面的程序不难理解,就不分析了。 #include iostream.h class CustomDate { public: int da, yr; CustomDate() {} CustomDate(int d,int y) { da=d; yr=y;} void display() { cout<<yr<<'-'<<da<<endl; } }; class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); for (int i=0;i<mo-1;i++) cd.da += dys[i]; cd.da+=da; return cd; } class Tester { CustomDate cd; public: explicit Tester(CustomDate c) { cd=c; } void display() { cd.display(); } }; void dispdate(CustomDate cd) { cd.display(); } CustomDate rtndate() { Date dt(9,11,1); return dt; } int main() { Date dt(12,24,3); CustomDate cd; cd = dt; cd.display(); dispdate(dt); Tester ts(dt); ts.display(); cd = rtndate(); cd.display(); return 0; } 六、显式构造函数 注意上面Tester类的构造函数前面有一个explicit修饰符。如果不加上这个关键字,那么在需要把CustomDate对象转换成Tester对象时,编译器会把该函数当作转换构造函数来调用。但是有时候,并不想把这种只有一个参数的构造函数用于转换目的,而仅仅希望用它来显式地初始化对象,此时,就需要在构造函数前加explicit。如果在声明了Tester对象以后使用了下面的语句将导致一个错误: ts=jd; //error 这个错误说明,虽然Tester类有一个以Date型变量为参数的构造函数,编译器却不会把它看作是从Date到Tester的转换构造函数,因为它的声明包含了explicit修饰符。 七、表达式内部的转换 在表达式内部,如果发现某个类型和需要的不一致,就会发生错误。数字类型的转换是很简单,这里就不举例了。下面的程序是把Date对象转换成长整型值。 #include iostream.h class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator long(); }; Date::operator long() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; long days=yr; days*=365; days+=(yr-1900)/4; //从1900年1月1日开始计算 for(int i=0;i<mo-1;i++) days+=dys[i]; days+=da; return days; } int main() { Date today(12,24,2003); const long ott=123; long sum=ott+today; cout<<ott<< + <<(long) today<< = <<sum; return 0; } 在表达式,当需要转换的对象可以转换成某个数字类型,或者表达式调用了作用于某个类的重载运算符时,就会发生隐式转换。运算符重载以后再学习。 主体:(四)私有数据成员和友元 一、私有数据成员的使用 1.取值和赋值成员函数 面向对象的约定就是保证所有数据成员的私有性。一般我们都是通过公有成员函数来作为公共接口来读取私有数据成员的。某些时候,我们称这样的函数为取值和赋值函数。 取值函数的返回值和传递给赋值函数的参数不必一一匹配所有数据成员的类型。 #include iostream.h class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } int getyear() const { return yr; } void setyear(int y) { yr = y; } }; int main() { Date dt(4,1,89); cout<<The year is: <<dt.getyear()<<endl; dt.setyear(97); cout<<The new year is: <<dt.getyear(); return 0; } 上面的例子很简单,不分析了。要养成这样的习惯,通过成员函数来访问和改变类的数据。这样有利于软件的设计和维护。比如,改变Date类内部数据的形式,但仍然用修改过的getyear()和setyear()来提供访问接口,那么使用该类就不必修改他们的代码,仅需要重新编译程序即可。 2.常量成员函数 注意上面的程序getyear()被声明为常量型,这样可以保证该成员函数不会修改调用他的对象。通过加上const修饰符,可以使访问对象数据的成员函数仅仅完成不会引起数据变动的那些操作。 如果程序声明某个Date对象为常量的话,那么该对象不得调用任何非常量型成员函数,不论这些函数是否真的试图修改对象的数据。只有把那些不会引起数据改变的函数都声明为常量型,才可以让常量对象来调用。 3.改进的成员转换函数 下面的程序改进了从Date对象到CustomDate对象的成员转换函数,用取值和赋值函数取代了使用公有数据成员的做法。(以前的程序代码在上一帖) #include iostream.h class CustomDate { int da,yr; public: CustomDate() {} CustomDate(int d,int y) { da=d; yr=y; } void display() const {cout<<yr<<'-'<<da<<endl;} int getday() const { return da; } void setday(int d) { da=d; } }; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate() const; }; Date::operator CustomDate() const { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); int day=da; for(int i=0;i<mo-1;i++) day+=dys[i]; cd.setday(day); return cd; } int main() { Date dt(11,17,89); CustomDate cd; cd=dt; cd.display(); return 0; } 注意上面的程序Date::operator CustomDate()声明为常量型,因为这个函数没有改变调用它对象的数据,尽管它修改了一个临时CustomDate对象并将其作为函数返回值。 二、友元 前面已经说过了,私有数据成员不能被类外的其他函数读取,但是有时候类会允许一些特殊的函数直接读写其私有数据成员。 关键字friend可以让特定的函数或者别的类的所有成员函数对私有数据成员进行读写。这既可以维护数据的私有性,有可以保证让特定的类或函数能够直接访问私有数据。 1.友元类 一个类可以声明另一个类为其友元,这个友元的所有成员函数都可以读写它的私有数据。 #include iostream.h class Date; class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } void display() const {cout<<yr<<'-'<<da<<endl;} friend Date; //这儿 }; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0, yr); for (int i=0;i<mo-1;i++) cd.da+=dys[i]; cd.da+=da; return cd; } int main() { Date dt(11,17,89); CustomDate cd(dt); cd.display(); return 0; } 在上面的程序,有这样一句 friend Date; 该语句告诉编译器,Date类的所有成员函数有权访问CustomDate类的私有成员。因为Date类的转换函数需要知道CustomDate类的每个数据成员,所以真个Date类都被声明为CustomDate类的友元。 2.隐式构造函数 上面程序对CustomDate的构造函数的调用私有显示该类需要如下的一个转换构造函数: CustomDate(Date& dt); 但是唯一的一个构造函数是:CustomDate(int d=0;int y=0); 这就出现了问题,编译器要从Date对象构造一个CustomDate对象,但是CustomDate类并没有定义这样的转换构造函数。不过Date类定义了一个成员转换函数,它可以把Date对象转换成CustomDate对象。于是编译器开始搜索CustomDate类,看其是否有一个构造函数,能从一个已存在的CustomDate的对象创建新的CustomDate对象。这种构造函数叫拷贝构造函数。拷贝构造函数也只有一个参数,该参数是它所属的类的一个对象,由于CustomDate类没有拷贝构造函数,于是编译器就会产生一个默认的拷贝构造函数,该函数简单地把已存在的对象的每个成员拷贝给新对象。现在我们已经知道,编译器可以把Date对象转换成CustomDate对象,也可以从已存在的CustomDate对象生成一个新的CustomDate对象。那么上面提出的问题,编译器就是这样做的:它首先调用转换函数,从Date对象创建一个隐藏的、临时的、匿名的CustomDate对象,然后用该临时对象作为参数调用默认拷贝构造函数,这就生成了一个新的CustomDate对象。 3.预引用 上面的例子还有这样一句 class Date; 这个语句叫做预引用。它告诉编译器,类Date将在后面定义。编译器必须知道这个信号,因为CustomDate类引用了Date类,而Date里也引用了CustomDate类,必须首先声明其之一。 使用了预引用后,就可以声明未定义的类的友元、指针和引用。但是不可以使用那些需要知道预引用的类的定义细节的语句,如声明该类的一个实例或者任何对该类成员的引用。 4.显式友元预引用 也可以不使用预引用,这只要在声明友元的时候加上关键自class就行了。 #include iostream.h class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } void display() const {cout<<yr<<'-'<<da<<endl;} friend class Date; //这儿,去掉前面的预引用 }; class Date { ... ... }; Date::operator CustomDate() { ... ... } int main() { ... ... } 5.友元函数 通常,除非真的需要,否则并不需要把整个类都设为另一个类的友元,只需挑出需要访问当前类私有数据成员的成员函数,将它们设置为该类的友元即可。这样的函数称为友元函数。 下面的程序限制了CustomDate类数据成员的访问,Date类只有需要这些数据的成员函数才有权读写它们。 #include iostream.h class CustomDate; class Date { int mo,da,yr; public: Date(const CustomDate&); void display() const {cout<<mo<<'/'<<da<<'/'<<yr<<endl;} }; class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } friend Date::Date(const CustomDate&); }; Date::Date(const CustomDate& cd) { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; yr=cd.yr; da=cd.da; for(mo=0;modys[mo]) da-=dys[mo]; else break; mo++; } int main() { Date dt(CustomDate(123, 89)); dt.display(); return 0; } 6.匿名对象 上面main()函数Date对象调用CustomDate类的构造函数创建了一个匿名CustomDate对象,然后用该对象创建了一个Date对象。这种用法在C++是经常出现的。 7.非类成员的友元函数 有时候友元函数未必是某个类的成员。这样的函数拥有类对象私有数据成员的读写权,但它并不是任何类的成员函数。这个特性在重载运算符时特别有用。 非类成员的友元函数通常被用来做为类之间的纽带。一个函数如果被两个类同时声明为友元,它就可以访问这两个类的私有成员。下面的程序说明了一个可以访问两个类私有数据成员的友元函数是如何将在两个类之间架起桥梁的。 #include iostream.h class Time; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y;} friend void display(const Date&, const Time&); }; class Time { int hr,min,sec; public: Time(int h,int m,int s) { hr=h; min=m; sec=s;} friend void display(const Date&, const Time&); }; void display(const Date& dt, const Time& tm) { cout << dt.mo << '/' << dt.da << '/' << dt.yr; cout << ' '; cout << tm.hr << ':' << tm.min << ':' << tm.sec; } int main() { Date dt(2,16,97); Time tm(10,55,0); display(dt, tm); return 0; } 主体:(五)析构函数和this指针 一、析构函数 前面的一些例子都没有说明析构函数,这是因为所用到的类在结束时不需要做特别的清理工作。下面的程序给出了一新的Date类,其包括一个字符串指针,用来表示月份。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void display() const; }; Date::Date(int m,int d,int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo=m; da=d; yr=y; if(m!=0) { month=new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if(month!=0) cout<<month<<' '<<da<<','<<yr; } int main() { Date birthday(8,11,1979); birthday.display(); return 0; } 在Date对象的构造函数,首先用new运算符为字符串month动态分配了内存,然后从内部数组把月份的名字拷贝给字符串指针month。 析构函数在删除month指针时,可能会出现一些问题。当然从这个程序本身来看,没什么麻烦;但是从设计一个类的角度来看,当Date类用于赋值时,就会出现问题。假设上面的main()修改为“ int main() { Date birthday(8,11,1979); Date today; today=birthday; birthday.display(); return 0; } 这会生成一个名为today的空的Date型变量,并且把birthday值赋给它。如果不特别通知编译器,它会简单的认为类的赋值就是成员对成员的拷贝。在上面的程序,变量birthday有一个字符型指针month,并且在构造函数里用new运算符初始化过了。当birthday离开其作用域时,析构函数会调用delete运算符来释放内存。但同时,当today离开它的作用域时,析构函数同样会对它进行释放操作,而today里的month指针是birthday里的month指针的一个拷贝。析构函数对同一指针进行了两次删除操作,这会带来不可预知的后果。 如果假设today是一个外部变量,而birthday是一个自变量。当birthday离开其作用域时,就已经把对象today里的month指针删除了。显然这也是不正确的。 再假设有两个初始化的Date变量,把其一个的值赋值给另一个: Date birthday(8,11,1979); Date today(12,29,2003); today=birthday; 问题就更复杂了,当这两个变量离开作用域时,birthday的month的值已经通过赋值传递给了today。而today构造函数用new运算符给month的值却因为赋值被覆盖了。这样,birthday的month被删除了两次,而todaymonth却没有被删除掉。 二、重载赋值运算符 为了解决上面的问题,我们应该写一个特殊的赋值运算符函数来处理这类问题。当需要为同一个类的两个对象相互赋值时,就可以重载运算符函数。这个方法可以解决类的赋值和指针的释放。 下面的程序,类的赋值函数用new运算符从堆分配了一个不同的指针,该指针获取赋值对象相应的值,然后拷贝给接受赋值的对象。 在类重载赋值运算符的格式如下: void operator = (const Date&) 后面我们回加以改进。目前,重载的运算符函数的返回类型为void。它是类总的成员函数,在本程序红,是Date类的成员函数。它的函数名始终是operator =,参数也始终是同一个类的对象的引用。参数表示的是源对象,即赋值数据的提供者。重载函数的运算符作为目标对象的成员函数来使用。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void operator=(const Date&); void display() const; }; Date::Date(int m, int d, int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo = m; da = d; yr = y; if (m != 0) { month = new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if (month!=0) cout<<month<<' '<<da<<,<<yr<<endl; } void Date::operator=(const Date& dt) { if (this != &dt) { mo = dt.mo; da = dt.da; yr = dt.yr; delete [] month; if (dt.month != 0) { month = new char [std::strlen(dt.month)+1]; std::strcpy(month, dt.month); } else month = 0; } } int main() { Date birthday(8,11,1979); birthday.display(); Date newday(12,29,2003); newday.display(); newday = birthday; newday.display(); return 0; } 除了为Date类加入了一个重载运算符函数,这个程序和上面的一个程序是相同的。赋值运算符函数首先取得所需的数据,然后用delete把原来的month指针所占用的内存返还给堆。接着,如果源对象的month指针已经初始化过,就用new运算符为对象重新分配内存,并把源对象的month字符串拷贝给接受方。 重载的Date类赋值运算符函数的第一个语句比较了源对象的地址和this指针。这个操作取保对象不会自己给自己赋值。 三、this指针 this指针是一个特殊的指针,当类的某个非静态的成员函数在执行时,就会存在this指针。它指向类的一个对象,且这个对象的某个成员函数正在被调用。 this指针的名字始终是this,而且总是作为隐含参数传递给每一个被声明的成员函数,例如: void Date::myFunc(Date* this); 实际编程时函数的声明不需要包含这个参数。 当程序调用某个对象的成员函数时,编译器会把该对象的地址加入到参数列表,感觉上就好象函数采用了上面所示的声明,并且是用如下方式来调用的: dt.myFunc(& dt); 静态成员函数不存在this指针。 当调用某个对象的成员函数时,编译器把对象的地址传递给this指针,然后再调用该函数。因此,成员函数你对任何成员的调用实际上都隐式地使用了this指针。 1.以this指针作为返回值 使用this指针可以允许成员函数返回调用对象给调用者。前面的程序重载赋值运算符没有返回值,因此不能用如下的形式对字符串进行赋值: a=b=c; 为了使重载的类赋值机制也能这样方便,必须让赋值函数返回赋值的结果,在这里就是目标对象。当赋值函数执行时,其返回值也恰好是this指针所指的内容。 下面的程序对前面那个程序进行了修改,让重载赋值运算符返回了一个Date对象的引用。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void operator=(const Date&); void display() const; }; Date::Date(int m, int d, int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo = m; da = d; yr = y; if (m != 0) { month = new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if (month!=0) cout<<month<<' '<<da<<,<<yr<<endl; } void Date::operator=(const Date& dt) { if (this != &dt) { mo = dt.mo; da = dt.da; yr = dt.yr; delete [] month; if (dt.month != 0) { month = new char [std::strlen(dt.month)+1]; std::strcpy(month, dt.month); } else month = 0; } return *this; } int main() { Date birthday(8,11,1979); Date oldday,newday; oldday=newday=birthday; birthday.display(); oldday.display(); newday.display(); return 0; } 2.在链表使用this指针 在应用程序,如果数据结构里有指向自身类型的成员,那么使用this指针会提供更多的方便。下面的程序建立了一个类ListEntry的链表。 #include iostream.h #include string.h class ListEntry { char* listvalue; ListEntry* preventry; public: ListEntry(char*); ~ListEntry() { delete [] listvalue; } ListEntry* PrevEntry() const { return preventry; }; void display() const { cout<<endl<<listvalue; } void AddEntry(ListEntry& le) { le.preventry = this; } }; ListEntry::ListEntry(char* s) { listvalue = new char[strlen(s)+1]; strcpy(listvalue, s); preventry = 0; } int main() { ListEntry* prev = 0; while (1) { cout <<endl<> name; if (strncmp(name, end, 3) == 0) break; ListEntry* list = new ListEntry(name); if (prev != 0) prev->AddEntry(*list); prev = list; } while (prev != 0) { prev->display(); ListEntry* hold = prev; prev = prev->PrevEntry(); delete hold; } return 0; } 程序运行时,会提示输入一串姓名,当输入完毕后,键入end,然后程序会逆序显示刚才输入的所有姓名。 程序ListEntry类含有一个字符串和一个指向前一个表项的指针。构造函数从对获取内存分配给字符串,并把字符串的内容拷贝到内存,然后置链接指针为NULL。析构函数将释放字符串所占用的内存。 成员函数PrevEntry()返回指向链表前一个表项的指针。另一个成员函数显示当前的表项内容。 成员函数AddEntry(),它把this指针拷贝给参数的preventry指针,即把当前表项的地址赋值给下一个表项的链接指针,从而构造了一个链表。它并没有改变调用它的listEntry对象的内容,只是把该对象的地址赋给函数的参数所引用的那个ListEntry对象的preventry指针,尽管该函数不会修改对象的数据,但它并不是常量型。这是因为,它拷贝对象的地址this指针的内容给一个非长常量对象,而编译器回认为这个非常量对象就有可能通过拷贝得到的地址去修改当前对象的数据,因此AddEntry()函数在声明时不需要用const。 主体:(六)类对象数组和静态成员 一、类对象数组 类的对象和C++其他数据类型一样,也可以为其建立数组,数组的表示方法和结构一样。 #include iostream.h class Date { int mo,da,yr; public: Date(int m=0,int d=0, int y=0) { mo=m; da=d; yr=y;} void display() const { cout<<mo<<'/'<<da<<'/'<<yr<<endl; } }; int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 1.类对象数组和默认构造函数 在前面已经说过,不带参数或者所有参数都有默认值的构造函数叫做默认构造函数。如果类没有构造函数,编译器会自动提供一个什么都不做的公共默认构造函数 。如果类当至少有一个构造函数,编译器就不会提供默认构造函数。 如果类当不含默认构造函数,则无法实例化其对象数组。因为实例花类对象数组的格式不允许用初始化值来匹配某个构造函数的参数表。 上面的程序,main()函数声明了一个长度为2的Date对象数组,还有一个包含初始化值的单个Date对象。接着把这个初始化的Date对象赋值给数组第一个对象,然后显示两个数组元素包含的日期。从输出可以看到,第一个日期是有效日期,而第二个显示的都是0。 当声明了某个类的对象数组时,编译器会为每个元素都调用默认构造函数。 下面的程序去掉了构造函数的默认参数值,并且增加了一个默认构造函数。 #include class Date { int mo, da, yr; public: Date(); Date(int m,int d,int y) { mo=m; da=d; yr=y;} void display() const { cout <<mo<<'/'<<da<<'/'<<yr<<endl; } }; Date::Date() { cout <<Date constructor running<<endl; mo=0; da=0; yr=0; } int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 运行程序,输出为: Date constructor running Date constructor running 12/31/2003 0/0/0 从输出可以看出,Date()这个默认构造函数被调用了两次。 2.类对象数组和析构函数 当类对象离开作用域时,编译器会为每个对象数组元素调用析构函数。 #include iostream.h class Date { int mo,da,yr; public: Date(int m=0,int d=0,int y=0) { mo=m; da=d; yr=y;} ~Date() {cout<<Date destructor running<<endl;} void display() const {cout<<mo<<'/'<<da<<'/'<<yr<<endl; } }; int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 运行程序,输出为: 12/31/2003 0/0/0 Date destructor running Date destructor running Date destructor running 表明析构函数被调用了三次,也就是dates[0],dates[1],today这三个对象离开作用域时调用的。 二、静态成员 可以把类的成员声明为静态的。静态成员只能存在唯一的实例。所有的成员函数都可以访问这个静态成员。即使没有声明类的任何实例,静态成员也已经是存在的。不过类当声明静态成员时并不能自动定义这个变量,必须在类定义之外来定义该成员。 1.静态数据成员 静态数据成员相当于一个全局变量,类的所有实例都可以使用它。成员函数能访问并且修改这个值。如果这个静态成员是公有的,那么类的作用域之内的所有代码(不论是在类的内部还是外部)都可以访问这个成员。下面的程序通过静态数据成员来记录链表首项和末项的地址。 #include iostream.h #include string.h class ListEntry { public: static ListEntry* firstentry; private: static ListEntry* lastentry; char* listvalue; ListEntry* nextentry; public: ListEntry(char*); ~ListEntry() { delete [] listvalue;} ListEntry* NextEntry() const { return nextentry; }; void display() const { cout<<listvalue<nextentry=this; lastentry=this; listvalue=new char[strlen(s)+1]; strcpy(listvalue,s); nextentry=0; } int main() { while (1) { cout<>name
c面试题 4. static有什么用途?(请至少说明两种) 1.限制变量的作用域 2.设置变量的存储域 7. 引用与指针有什么区别? 1) 引用必须被初始化,指针不必。 2) 引用初始化以后不能被改变,指针可以改变所指的对象。 2) 不存在指向空值的引用,但是存在指向空值的指针。 8. 描述实时系统的基本特性 在特定时间内完成特定的任务,实时性与可靠性 9. 全局变量和局部变量在内存是否有区别?如果有,是什么区别? 全局变量储存在静态数据库,局部变量在堆栈 10. 什么是平衡二叉树? 左右子树都是平衡二叉树且左右子树的深度差值的绝对值不大于1 11. 堆栈溢出一般是由什么原因导致的? 没有回收垃圾资源 12. 什么函数不能声明为虚函数? constructor 13. 冒泡排序算法的时间复杂度是什么? O(n^2) 14. 写出float x 与“零值”比较的if语句。 if(x>0.000001&&x<-0.000001) 16. Internet采用哪种网络协议?该协议的主要层次结构? tcp/ip 应用层/传输层/网络层/数据链路层/物理层 17. Internet物理地址和IP地址转换采用什么协议? ARP (Address Resolution Protocol)(地址解析協議) 18.IP地址的编码分为哪俩部分? IP地址由两部分组成,网络号和主机号。不过是要和“子网掩码”按位与上之后才能区分哪些是网络位哪些是主机位。 2.用户输入M,N值,从1至N开始顺序循环数数,每数到M输出该数值,直至全部输出。写出C程序。 循环链表,用取余操作做 3.不能做switch()的参数类型是: switch的参数不能为实型。 華為 1、局部变量能否和全局变量重名? 答:能,局部会屏蔽全局。要用全局变量,需要使用"::" 局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内 2、如何引用一个已经定义过的全局变量? 答:extern 可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个在头文件声明的全局变理,假定你将那个变写错了,那么在编译期间会报错,如果你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期间报错 3、全局变量可不可以定义在可被多个.C文件包含的头文件?为什么? 答:可以,在不同的C文件static形式来声明同名全局变量。 可以在不同的C文件声明同名的全局变量,前提是其只能有一个C文件对此变量赋初值,此时连接不会出错 4、语句for( ;1 ;)有什么问题?它是什么意思? 答:和while(1)相同。 5、do……while和while……do有什么区别? 答:前一个循环一遍再判断,后一个判断以后再循环 6、请写出下列代码的输出内容 #include<stdio.h> main() { int a,b,c,d; a=10; b=a++; c=++a; d=10*a++; printf("b,c,d:%d,%d,%d",b,c,d); return 0; } 答:10,12,120 1、static全局变量与普通的全局变量有什么区别?static局部变量和普通局部变量有什么区别?static函数与普通函数有什么区别? 全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件引起错误。 从以上分析可以看出,把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。 static函数与普通函数作用域不同。仅在本文件。只在当前源文件使用的函数应该说明为内部函数(static),内部函数应该在当前源文件说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件说明,要使用这些函数的源文件要包含这个头文件 static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元被引用; static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值; static函数与普通函数有什么区别:static函数在内存只有一份,普通函数在每个被调用维持一份拷贝 2、程序的局部变量存在于(堆栈),全局变量存在于(静态区),动态申请数据存在于(堆)。 3、设有以下说明和定义: typedef union {long i; int k[5]; char c;} DATE; struct data { int cat; DATE cow; double dog;} too; DATE max; 则语句 printf("%d",sizeof(struct date)+sizeof(max));的执行结果是:___52____ 答:DATE是一个union, 变量公用空间. 里面最大的变量类型是int[5], 占用20个字节. 所以它的大小是20 data是一个struct, 每个变量分开占用空间. 依次为int4 + DATE20 + double8 = 32. 所以结果是 20 + 32 = 52. 当然...在某些16位编辑器下, int可能是2字节,那么结果是 int2 + DATE10 + double8 = 20 4、队列和栈有什么区别? 队列先进先出,栈后进先出 5、写出下列代码的输出内容 #include<stdio.h> int inc(int a) { return(++a); } int multi(int*a,int*b,int*c) { return(*c=*a**b); } typedef int(FUNC1)(int in); typedef int(FUNC2) (int*,int*,int*); void show(FUNC2 fun,int arg1, int*arg2) { INCp=&inc; int temp =p(arg1); fun(&temp,&arg1, arg2); printf("%d"n",*arg2); } main() { int a; show(multi,10,&a); return 0; } 答:110 7、请找出下面代码的所以错误 说明:以下代码是把一个字符串倒序,如“abcd”倒序后变为“dcba” 1、#include"string.h" 2、main() 3、{ 4、 char*src="hello,world"; 5、 char* dest=NULL; 6、 int len=strlen(src); 7、 dest=(char*)malloc(len); 8、 char* d=dest; 9、 char* s=src[len]; 10、 while(len--!=0) 11、 d++=s--; 12、 printf("%s",dest); 13、 return 0; 14、} 答: 方法1: int main(){ char* src = "hello,world"; int len = strlen(src); char* dest = (char*)malloc(len+1);//要为"0分配一个空间 char* d = dest; char* s = &src[len-1];//指向最后一个字符 while( len-- != 0 ) *d++=*s--; *d = 0;//尾部要加"0 printf("%s"n",dest); free(dest);// 使用完,应当释放空间,以免造成内存汇泄露 return 0; } 方法2: #include <stdio.h> #include <string.h> main() { char str[]="hello,world"; int len=strlen(str); char t; for(int i=0; i<len/2; i++) { t=str[i]; str[i]=str[len-i-1]; str[len-i-1]=t; } printf("%s",str); return 0; } 1.-1,2,7,28,,126请问28和126间那个数是什么?为什么? 第一题的答案应该是4^3-1=63 规律是n^3-1(当n为偶数0,2,4) n^3+1(当n为奇数1,3,5) 答案:63 2.用两个栈实现一个队列的功能?要求给出算法和思路! 设2个栈为A,B, 一开始均为空. 入队: 将新元素push入栈A; 出队: (1)判断栈B是否为空; (2)如果不为空,则将栈A所有元素依次pop出并push到栈B; (3)将栈B的栈顶元素pop出; 这样实现的队列入队和出队的平摊复杂度都还是O(1), 比上面的几种方法要好。3.在c语言库函数将一个字符转换成整型的函数是atool()吗,这个函数的原型是什么? 函数名: atol 功能: 把字符串转换成长整型数 用法: long atol(const char *nptr); 程序例: #include <stdlib.h> #include <stdio.h> int main(void) { long l; char *str = "98765432"; l = atol(lstr); printf("string = %s integer = %ld"n", str, l); return(0); } 2.对于一个频繁使用的短小函数,在C语言应用什么实现,在C++应用什么实现? c用宏定义,c++用inline 3.直接链接两个信令点的一组链路称作什么? PPP点到点连接 4.接入网用的是什么接口? 5.voip都用了那些协议? 6.软件测试都有那些种类? 黑盒:针对系统功能的测试白合:测试函数功能,各函数接口 7.确定模块的功能和模块的接口是在软件设计的那个队段完成的? 概要设计阶段 8.enum string { x1, x2, x3=10, x4, x5, }x; 问x= 0x801005,0x8010f4 ; 9.unsigned char *p1; unsigned long *p2; p1=(unsigned char *)0x801000; p2=(unsigned long *)0x810000; 请问p1+5= ; p2+5= ; 三.选择题: 1.Ethternet链接到Internet用到以下那个协议? A.HDLC;B.ARP;C.UDP;D.TCP;E.ID 2.属于网络层协议的是: A.TCP;B.IP;C.ICMP;D.X.25 3.Windows消息调度机制是: A.指令队列;B.指令堆栈;C.消息队列;D.消息堆栈; 4.unsigned short hash(unsigned short key) { return (key>>)%256 } 请问hash(16),hash(256)的值分别是: A.1.16;B.8.32;C.4.16;D.1.32 四.找错题: 1.请问下面程序有什么错误? int a[60][250][1000],i,j,k; for(k=0;k<=1000;k++) for(j=0;j<250;j++) for(i=0;i<60;i++) a[i][j][k]=0; 把循环语句内外换一下 2.#define Max_CB 500 void LmiQueryCSmd(Struct MSgCB * pmsg) { unsigned char ucCmdNum; ...... for(ucCmdNum=0;ucCmdNum<Max_CB;ucCmdNum++) { ......; } 死循环 3.以下是求一个数的平方的程序,请找出错误: #define SQUARE(a)((a)*(a)) int a=5; int b; b=SQUARE(a++); 4.typedef unsigned char BYTE int examply_fun(BYTE gt_len; BYTE *gt_code) { BYTE *gt_buf; gt_buf=(BYTE *)MALLOC(Max_GT_Length); ...... if(gt_len>Max_GT_Length) { return GT_Length_ERROR; } ....... } 五.问答题: 1.IP Phone的原理是什么? IPV6 2.TCP/IP通信建立的过程怎样,端口有什么作用? 三次握手,确定是哪个应用程序使用该协议 3.1号信令和7号信令有什么区别,我国某前广泛使用的是那一种? 4.列举5种以上的电话新业务?

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值