GCC是Linux平台下最重要的开发工具,它是GNU的C和C++编译器,其基本用法为:
gcc [options] [filenames]
options为编译选项,GCC总共提供的编译选项超过100个,但只有少数几个会被频繁使用,我们仅对几个常用选项进行介绍。
假设我们编译一输出“Hello World”的程序:
/* Filename:helloworld.c */
int main()
{
printf("Hello World\n");
return 0;
}`/* 何问起 hovertree.com */`
最简单的编译方法是不指定任何编译选项: gcc helloworld.c
它会为目标程序生成默认的文件名a.out,我们可用-o编译选项来为将产生的可执行文件指定一个文件名来代替a.out。
例如,将上述名为 helloworld.c 的 C 程序编译为名叫helloworld的可执行文件,需要输入如下命令:
gcc –o helloworld helloworld.c
-c 选项告诉GCC仅把源代码编译为目标代码而跳过汇编和连接的步骤;
-S 编译选项告诉 GCC 在为 C代码产生了汇编语言文件后停止编译。GCC 产生的汇编语言文件的缺省扩展名是.s,上述程序运行如下命令:
[root@ecs-b0b7 c]# gcc -S helloworld.c
将生成helloworld.c的汇编代码,使用的是AT&T汇编。
`
[root@ecs-b0b7 c]# vim helloworld.s
.file "helloworld.c"
.section .rodata
.LC0:
.string "Hello World"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $.LC0, %edi
call puts
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)"
.section .note.GNU-stack,"",@progbits
`
-E 选项指示编译器仅对输入文件进行预处理。当这个选项被使用时,
预处理器的输出被送到标准输出(默认为屏幕)而不是储存在文件里。
将头文件引用全部展开:
`
[root@ecs-b0b7 c]# gcc -E helloworld.c | head -n 20
# 1 "helloworld.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "helloworld.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 375 "/usr/include/features.h" 3 4
# 1 "/usr/include/sys/cdefs.h" 1 3 4
# 392 "/usr/include/sys/cdefs.h" 3 4
# 1 "/usr/include/bits/wordsize.h" 1 3 4
# 393 "/usr/include/sys/cdefs.h" 2 3 4
# 376 "/usr/include/features.h" 2 3 4
# 399 "/usr/include/features.h" 3 4
# 1 "/usr/include/gnu/stubs.h" 1 3 4
# 10 "/usr/include/gnu/stubs.h" 3 4
# 1 "/usr/include/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/gnu/stubs.h" 2 3 4
`
-O 选项告诉GCC对源代码进行基本优化从而使得程序执行地更快;而-O2选项告诉GCC产生尽可能小和尽可能快的代码。
使用-O2选项编译的速度比使用-O时慢,但产生的代码执行速度会更快。
-g 选项告诉GCC产生能被GNU调试器使用的调试信息以便调试你的程序,可喜的是,在GCC里,我们能联用-g和-O (产生优化代码)。
-pg 选项告诉GCC在你的程序里加入额外的代码,执行时,产生gprof用的剖析信息以显示你的程序的耗时情况。
`
[root@ecs-b0b7 c]# gcc helloworld.c -O3 -g -pg -o helloworldO3
[root@ecs-b0b7 c]# gprof helloworldO3
Flat profile:
Each sample counts as 0.01 seconds.
no time accumulated
% cumulative self self total
time seconds seconds calls Ts/call Ts/call name
% the percentage of the total running time of the
time program used by this function.
cumulative a running sum of the number of seconds accounted
seconds for by this function and those listed above it.
self the number of seconds accounted for by this
seconds function alone. This is the major sort for this
listing.
calls the number of times this function was invoked, if
this function is profiled, else blank.
self the average number of milliseconds spent in this
ms/call function per call, if this function is profiled,
else blank.
total the average number of milliseconds spent in this
ms/call function and its descendents per call, if this
function is profiled, else blank.
name the name of the function. This is the minor sort
for this listing. The index shows the location of
the function in the gprof listing. If the index is
in parenthesis it shows where it would appear in
the gprof listing if it were to be printed.
Copyright (C) 2012-2016 Free Software Foundation, Inc.
Copying and distribution of this file, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved.
[root@ecs-b0b7 c]#
`
3、GDB调试器
GCC用于编译程序,而 Linux 的另一个 GNU 工具 gdb 则用于调试程序。
gdb 是一个用来调试 C 和 C++ 程序的强力调试器,我们能通过它进行一
系列调试工作,包括设置断点、观查变量、单步等。
其最常用的命令如下:
file:装入想要调试的可执行文件。
kill:终止正在调试的程序。
list:列表显示源代码。
next:执行一行源代码但不进入函数内部。
step:执行一行源代码而且进入函数内部。
run:执行当前被调试的程序
quit:终止gdb
watch:监视一个变量的值
break:在代码里设置断点,程序执行到这里时挂起
make:不退出gdb而重新产生可执行文件
shell:不离开gdb而执行shell
下面我们来演示怎样用GDB来调试一个求0+1+2+3+…+99的程序:
/* Filename:sum.c */
void main()
{
int i, sum;
sum = 0;
for (i = 0; i < 100; i++)
{
sum + = i;
}
printf("the sum of 1+2+...+ is %d", sum);
}
执行如下命令编译sum.c(加-g选项产生debug信息): gcc –g –o sum sum.c
在命令行上键入gdb sum并按回车键就可以开始调试sum了,再运行run命令执行sum,屏幕上将看到如下内容:
list命令:list命令用于列出源代
这个时候我们再run,程序会停止在第5行,如下图:
设置断点的另一种语法是 break <function>,它在进入指定函数(function)时停住。
clear 用于清除所有的已定义的断点
clear lineNo
clear functionName
delete
delete lineNo
delete breakPointNum
watch命令:
watch命令用于观查变量或表达式的值,我们观查sum变量只需要运行watch sum
watch <expr>为表达式(变量)expr设置一个观察点,一量表达式值有变化时,程序会停止执行。
要观查当前设置的watch,可以使用info watchpoints命令。
next、step命令:
next、step用于单步执行,在执行的过程中,被watch变量的变化情况将实时呈现(分别显示Old value和New value),如下图:
next、step命令的区别在于step遇到函数调用,会跳转到到该函数定义的开始行去执行,而next则不进入到函数内部,它把函数调用语句当作
4、Make
make 是所有想在 Linux 系统上编程的用户必须掌握的工具,对于任何稍具规模的程序,
我们都会使用到make,几乎可以说不使用make的程序不具备任何实用价值。
在此,我们有必要解释编译和连接的区别。编译器使用源码文件来产生某种形式的目标文件(object files),在编译过程中,外部的符号参考
并没有被解释或替换(即外部全局变量和函数并没有被找到)。因此,在编译阶段所报的错误一般都是语法错误。而连接器则用于连接目标文
件和程序包,生成一个可执行程序。在连接阶段,一个目标文件中对别的文件中的符号的参考被解释,
如果有符号不能找到,会报告连接错误
编译和连接的一般步骤是:第一阶段把源文件一个一个的编译成目标文件,
第二阶段把所有的目标文件加上需要的程序包连接成一个可执行文件。
这样的过程很痛苦,我们需要使用大量的 gcc 命令。
而 make 则使我们从大量源文件的编译和连接工作中解放出来,综合为一步完成。
GNU Make 的主要工作是读进一个文本文件,称为 makefile。
这个文件记录了哪些文件(目的文件,目的文件不一定是最后的可执行程序,它可以是任何一种文件)由哪些文件(依靠文件)产生,用什么命令来产生。
Make依靠此makefile中的信息检查磁盘上的文件,如果目的文件的创建或修改时间比它的一个依靠文件旧的话,make就执行相应的命令,以便更新目的文件。
假设我们写下如下的三个文件,add.h用于声明add函数,add.c提供两个整数相加的函数体,而main.c中调用add函数:
`/* filename:add.h */
extern int add(int i, int j);`
`/* filename:add.c */
int add(int i, int j)
{
return i + j;
};`
`/* filename:main.c */
#include "add.h"
main()
{
int a, b;
a = 2;
b = 3;
printf("the sum of a+b is %d", add(a + b));
};`
怎样为上述三个文件编写 makefile 呢?如下:
-------------------------
test : main.o add.o
gcc main.o add.o -o test
main.o : main.c add.h
gcc -c main.c -o main.o
add.o : add.c add.h
gcc -c add.c -o add.o
-----------------------
上述 makefile 利用 add.c 和 add. h文件执行 gcc -c add.c -o add.o 命令产生add.o目标代码,利用main.c和add.h文件执行gcc -c main.c -o
main.o 命令产生 main.o 目标代码,最后利用 main.o 和 add.o 文件(两个模块的目标代码)执行gcc main.o add.o -o test命令产生可执行文件 test。
我们可在 makefile 中加入变量,另外。
环境变量在make过程中也被解释成make的变量。这些变量是大小写敏感的,一般使用大写字母。
Make变量可以做很多事情,例如:
i) 存储一个文件名列表;
ii) 存储可执行文件名;
iii) 存储编译器选项。
要定义一个变量,只需要在一行的开始写下这个变量的名字,后面跟一个=号,再跟变量的值。
引用变量的方法是写一个$符号,后面跟(变量名)。
我们把前面的 makefile 利用变量重写一遍(并假设使用-Wall -O –g编译选项):
`
OBJS = main.o add.o
CC = gcc
CFLAGS = -Wall -O -g
test : $(OBJS)
$(CC) $(OBJS) -o test
main.o : main.c add.h
$(CC) $(CFLAGS) -c main.c -o main.o
add.o : add.c add.h
$(CC) $(CFLAGS) -c add.c -o add.o
`
makefile 中还可定义清除(clean)目标,可用来清除编译过程中产生的中间文件,例如在上述makefile文件中添加下列代码:
`
clean:
rm -f *.o
`
运行 make clean 时,将执行rm -f *.o命令,删除所有编译过程中产生的中间文件。
不管怎么说,
自己动手编写 makefile 仍然是很复杂和烦琐的,而且很容易出错。
因此,GNU 也为我们提供了 Automake 和 Autoconf 来辅助快速自动产生makefile,读者可以参阅相关资料。
5、小结
本章主要阐述了 Linux 程序的编写、编译、调试方法及 make,
实际上就是引导读者学习怎样在 Linux 下编程,为后续章节做好准备
二、Linux下的C编程实战(二)
文件系统编程
1、Linux文件系统
Linux 支持多种文件系统,如 ext、ext2、minix、iso9660、msdos、fat、vfat、nfs等。
在这些具体文件系统的上层,Linux提供了虚拟文件系统(VFS)来统一它们的行为,虚拟文件系统为不同的文件系统与内核的通信提供了一致的接口。
下图给出了 Linux 中文件系统的关系(缺失)
在 Linux 平台下对文件编程可以使用两类函数:
(1)Linux操作系统文件API;
(2)C语言I/O库函数。
前者依赖于Linux系统调用,后者实际上与操作系统是独立的,因为在任何操作系统下,
使用C语言I/O库函数操作文件的方法都是相同的。
本章将对这两种方法进行实例讲解。
进程控制中主要涉及到进程的创建、睡眠和退出等,在Linux中主要提供了fork、exec、clone的进程创建方法,sleep的进程睡眠和exit的进程
退出调用,另外Linux还提供了父进程等待子进程结束的系统调用wait。
fork
对于没有接触过Unix/Linux操作系统的人来说,fork是最难理解的概念之一,它执行一次却返回两个值,完全“不可思议”。先看下面的程序
// fork 不需要依赖其它头文件
#include <stdio.h>
int main()
{
int i = 0;
if (fork() == 0)
{
for (i=1; i<=2; i++)
{
printf("this is child process\n");
}
}
else
{
for (i=1; i<=2; i++)
{
printf("this is child process\n");
}
}
return 0;
}
执行结果为:
This is child process
This is child process
This is parent process
This is parent process
fork在英文中是“分叉”的意思,这个名字取得很形象。一个进程在运行中,如果使用了fork,就产生了另一个进程,于是进程就“分叉”了。
当前进程为父进程,通过fork()会产生一个子进程。对于父进程,fork函数返回子程序的进程号而对于子程序,fork函数则返回零,这就是
一个函数返回两次的本质。可以说,fork函数是Unix系统最杰出的成就之一,它是七十年代Unix早期的开发者经过理论和实践上的长期艰苦探索后取得的成果。
如果我们把上述程序中的循环放的大一点:
int main()
{
int i;
if (fork() == 0)
{
for (i = 1; i < 10000; i++)
printf("This is child process\n");
}
else
{
for (i = 1; i < 10000; i++)
printf("This is parent process\n");
}
};
则可以明显地看到父进程和子进程的并发执行,交替地输出“This is child process”和“This is parent process”,事实上将其改为30时,就能够开始看到有交替打印。
此时此刻,我们还没有完全理解fork()函数,再来看下面的一段程序,看看究竟会产生多少个进程,程序的输出是什么?
[root@localhost ssss]# cat multiFork.c
int main()
{
int i;
int j=0;
for (i = 0; i < 2; i++)
{
if (fork() == 0)
{
printf("This is child process:%d\n", j++);
}
else
{
printf("This is parent process:%d\n", j++);
}
}
return 0;
};
[root@localhost ssss]# ./a.out
This is parent process:0
This is parent process:1
This is child process:1
This is child process:0
This is parent process:1
This is child process:1
[root@localhost ssss]#
clone 是 Linux2.0 以后才具备的新功能,它较 fork 更强(可认为 fork 是 clone 要实现的一部分),可以使得创建的子进程共享父进程的资源,
并且要使用此函数必须在编译内核时设置clone_actually_works_ok选项。
clone函数的原型为:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
此函数返回创建进程的PID,函数中的flags标志用于设置创建子进程时的相关选项,具体含义如下表:
=====================================================================================
标志 含义
CLONE_PARENT 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask
CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
CLONE_NEWNS 在新的namespace启动子进程,namespace描述了进程的文件hierarchy
CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
CLONE_PTRACE 若父进程被trace,子进程也被trace
CLONE_VFORK 父进程被挂起,直至子进程释放虚拟内存资源
CLONE_VM 子进程与父进程运行于相同的内存空间
CLONE_PID 子进程在创建时PID与父进程一致
CLONE_THREAD Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群
来看下面的例子:
#include <stdio.h>
#include <linux/sched.h>
#include <fcntl.h>
#include <unistd.h>
int variable, fd;
int do_something() {
variable = 42;
close(fd);
printf("in new function cloned...\n");
_exit(0);
}
int main(int argc, char *argv[])
{
void **child_stack;
char tempch;
variable = 9;
fd = open("aa", O_RDONLY);
child_stack = (void **) malloc(16384);
printf("The variable was %d\n", variable);
clone(do_something, child_stack, CLONE_VM|CLONE_FILES, NULL);
sleep(3); /* 延时以便子进程完成关闭文件操作、修改变量 */
printf("The variable is now %d\n", variable);
if (read(fd, &tempch, 1) < 1) {
perror("File Read Error");
exit(1);
}
printf("We could read from the file\n");
return 0;
}
运行输出,很奇怪:
[root@localhost ssss]# gcc -g cloneTest.c
直接运行,崩溃,因为在 do_something 函数退出时崩溃了,没有打印,好像没有进入 do_something
[root@localhost ssss]# ./a.out
The variable was 9
段错误
[root@localhost ssss]#
采用 gdb 调试,却表现出不同的行为:
从截图上看,显然它进入到了 do_something 函数,而且输出了调试打印机,但遇到 exit(0) 后,直接崩溃
程序到的 CLONE_VM、CLONE_FILES 标志将使得变量和文件描述符表被共享),
父进程随即就感觉到了,这就是clone的特点。
sleep
函数调用sleep可以用来使进程挂起指定的秒数,该函数的原型为:
unsigned int sleep(unsigned int seconds);
该函数调用使得进程挂起一个指定的时间,如果指定挂起的时间到了,该调用返回0;
如果该函数调用被信号所打断,则返回剩余挂起的时间数(指定的时间减去已经挂起的时间)。
Linux 的进程间通信(IPC,InterProcess Communication)
通信方法有 管道、消息队列、共享内存、信号量、套接口等。
管道分为有名管道和无名管道,无名管道只能用于亲属进程之间的通信,而有名管道则可用于无亲属关系的进程之间。
#include <stdio.h>
#include <linux/sched.h>
#include <fcntl.h>
#include <unistd.h>
#define INPUT 0
#define OUTPUT 1
#define BUFFER_LEN 1024
void main()
{
int file_descriptors[2];
/*定义子进程号 */
pid_t pid;
char buf[BUFFER_LEN];
int returned_count;
/*创建无名管道*/
pipe(file_descriptors);
/*创建子进程*/
if ((pid = fork()) == -1)
{
printf("Error in fork\n");
exit(1);
}
/*执行子进程*/
if (pid == 0)
{
printf("in the spawned (child) process...\n");
/*子进程向父进程写数据,关闭管道的读端*/
close(file_descriptors[INPUT]);
write(file_descriptors[OUTPUT], "test data", strlen("test data"));
exit(0);
}
else
{
/*执行父进程*/
printf("in the spawning (parent) process...\n");
/*父进程从管道读取子进程写的数据,关闭管道的写端*/
close(file_descriptors[OUTPUT]);
returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
printf("%d bytes of data received from spawned process: %s\n", returned_count, buf);
}
}
[root@localhost ssss]# ./a.out
in the spawning (parent) process...
in the spawned (child) process...
9 bytes of data received from spawned process: test data
[root@localhost ssss]#
上述程序中,无名管道以 int pipe(int filedis[2]);
方式定义,参数 filedis 返回两个文件描述符filedes[0]为读而打开,
filedes[1]为写而打开,filedes[1]的输出是filedes[0]的输入;
在Linux系统下,有名管道可由两种方式创建(假设创建一个名为“fifoexample”的有名管道):
(1)mkfifo("fifoexample","rw");
(2)mknod fifoexample p
mkfifo 是一个函数,mknod 是一个系统调用,即我们可以在shell下输出上述命令。
有名管道创建后,我们可以像读写文件一样读写之:
[root@localhost ssss]# cat writePipe.c
#include <stdio.h>
#include <linux/sched.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFER_LEN 128
/* 进程二:写有名管道*/
void main()
{
FILE *out_file;
int count = 1;
char buf[BUFFER_LEN];
out_file = fopen("pipeexample", "w");
if (out_file == NULL)
{
printf("Error opening pipe.");
exit(1);
}
sprintf(buf, "this is test data for the named pipe example\n");
fwrite(buf, 1, BUFFER_LEN, out_file);
fclose(out_file);
}
[root@localhost ssss]# cat readPipe.c
#include <stdio.h>
#include <linux/sched.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFER_LEN 128
/* 进程一:读有名管道*/
void main()
{
FILE *in_file;
int count = 1;
char buf[BUFFER_LEN];
in_file = fopen("pipeexample", "r");
if (in_file == NULL)
{
printf("Error in fdopen.\n");
exit(1);
}
while ((count = fread(buf, 1, BUFFER_LEN, in_file)) > 0)
{
printf("received from pipe: %s\n", buf);
}
fclose(in_file);
}
[root@localhost ssss]# ./writePipe
[root@localhost ssss]# ./readPipe
received from pipe: this is test data for the named pipe example
消息队列用
消息队列用于运行于同一台机器上的进程间通信,与管道相似;
共享内存通常由一个进程创建,其余进程对这块内存区进行读写。
得到共享内存有两种方式:映射/dev/mem设备 和 内存映像文件。
前一种方式不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的是实际的物理内存;常用的方式是通过shmXXX函数族来实现共享内存:
int shmget(key_t key, int size, int flag); /* 获得一个共享存储标识符 */
该函数使得系统分配size大小的内存用作共享内存;
void *shmat(int shmid, void *addr, int flag); /* 将共享内存连接到自身地址空间中*/
shmid 为 shmget 函数返回的共享存储标识符,addr 和 flag 参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址。
此后,进程可以对此地址进行读写操作访问共享内存。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。
一般说来,为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量;
(2)若此信号量的值为正,则允许进行使用该资源,进程将进号量减1;
(3)若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1);
(4)当进程不再使用一个信号量控制的资源时,信号量值加1,如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
下面是一个使用信号量的例子,该程序创建一个特定的 IPC 结构的关键字和一个信号量,建立此信号量的索引,修改索引指向的信号量的值,最后清除信号量:
#include <stdio.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
/* union semun is defined by including <sys/sem.h> */
#else
/* according to X/OPEN we have to define it ourselves */
union semun {
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* array for GETALL, SETALL */
/* Linux specific part: */
struct seminfo *__buf; /* buffer for IPC_INFO */
};
#endif
void main()
{
key_t unique_key; /* 定义一个IPC关键字*/
int id;
struct sembuf lock_it;
union semun options;
int i;
unique_key = ftok(".", 'a'); /* 生成关键字,字符'a'是一个随机种子*/
/* 创建一个新的信号量集合*/
id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);
printf("semaphore id=%d\n", id);
options.val = 1; /*设置变量值*/
semctl(id, 0, SETVAL, options); /*设置索引0的信号量*/
/*打印出信号量的值*/
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d\n", i);
/*下面重新设置信号量*/
lock_it.sem_num = 0; /*设置哪个信号量*/
lock_it.sem_op = - 1; /*定义操作*/
lock_it.sem_flg = IPC_NOWAIT; /*操作方式*/
if (semop(id, &lock_it, 1) == - 1)
{
printf("can not lock semaphore.\n");
exit(1);
}
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d\n", i);
/*清除信号量*/
semctl(id, 0, IPC_RMID, 0);
}
[root@localhost ssss]# ./semTest
semaphore id=131072
value of semaphore at index 0 is 1 ==>> 表明此时有一个信号量可以获取
value of semaphore at index 0 is 0 ==>> 表明此时已经没有信号量可用
[root@localhost ssss]#
套接字
套接字通信并不为Linux所专有,在所有提供了TCP/IP协议栈的操作系统中几乎都提供了socket,而所有这样操作系统,对套接字的编程方法几
乎是完全一样的。
4.小节
本章讲述了 Linux 进程的概念,并以多个实例讲解了进程控制及进程间通信方法,理解这一章的内容可以说是理解 Linux 这个操作系统的关键。
function confirmSelect() {
var province=("#provinceSelect option:selected"); var cityText=("#citySelect option:selected")
var ISP=("#ISP option:selected") //得到三个下拉框的值,将它们合并成一个字符串 var auxData=province.text()+','+cityText.text()+','+ISP.text(); //将合并后的值赋值给隐藏控件 ('#auxData').val(auxData);
//最后调用<form>的submit函数
$("#allData").submit();
}
四、js 怎样根据控件ID,定位它的位置
1、方法一:js原生方法
var oDiv = document.getElementById('divID');
alert(oDiv.getBoundingClientRect().left)
alert(oDiv.getBoundingClientRect().top)
2、方法二,为什么要这么费事
function getElementPos(elementId) {
var ua = navigator.userAgent.toLowerCase();
var isOpera = (ua.indexOf('opera') != -1);
var isIE = (ua.indexOf('msie') != -1 && !isOpera); // not opera spoof
var el = document.getElementById(elementId);
if (el.parentNode === null ||
el.style.display == 'none') {
return false;
}
var parent = null;
var pos = [];
var box;
if (el.getBoundingClientRect) //IE
{
box = el.getBoundingClientRect();
var scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop);
var scrollLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft);
return {
x: box.left + scrollLeft,
y: box.top + scrollTop
};
}
else if (document.getBoxObjectFor) //gecko
{
box = document.getBoxObjectFor(el);
var borderLeft = (el.style.borderLeftWidth) ? parseInt(el.style.borderLeftWidth) : 0;
var borderTop = (el.style.borderTopWidth) ? parseInt(el.style.borderTopWidth) : 0;
pos = [box.x - borderLeft, box.y - borderTop];
}
else // safari & opera
{
pos = [el.offsetLeft, el.offsetTop];
parent = el.offsetParent;
if (parent != el) {
while (parent) {
pos[0] += parent.offsetLeft;
pos[1] += parent.offsetTop;
parent = parent.offsetParent;
}
}
if (ua.indexOf('opera') != -1 ||
(ua.indexOf('safari') != -1 &&
el.style.position == 'absolute')) {
pos[0] -= document.body.offsetLeft;
pos[1] -= document.body.offsetTop;
}
}
if (el.parentNode) {
parent = el.parentNode;
} else {
parent = null;
}
while (parent &&
parent.tagName != 'BODY' && parent.tagName != 'HTML') { // account for any scrolled ancestors
pos[0] -= parent.scrollLeft;
pos[1] -= parent.scrollTop;
if (parent.parentNode) {
parent = parent.parentNode;
} else {
parent = null;
}
}
return {
x: pos[0],
y: pos[1]
};
}
修改:
if(_arr_answer_length==4){
for (j=0;j<4; j++){
try{
xxx
switch(j){
case 0:
case 1:
case 2:
case 3:
}
}
}
}
这一段改为
两层循环变成一层,不用写死4,内部答案可以最多7个,到'H':
for (j=0;j<_arr_answer_length; j++){}
同事,将少于4个选项的异常 else 注释掉