本文共 48824 字,大约阅读时间需要 162 分钟。
内存分配 一个由C/C++编译的程序占用的内存分为以下几个部分 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 2、堆区(heap)— 由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 3、全局区(静态区)(static)— 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。 4、文字常量区 — 常量字符串就是放在这里的,程序结束后由系统释放 。 5、程序代码区— 存放函数体的二进制代码。 常用数据结构: 1,数组 (Array) 2,栈 (Stack) 是只能在某一端插入和删除的特殊线性表。它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。 3,队列 (Queue) 一种特殊的线性表,它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。 4,链表 (Linked List) 是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 5,树 (Tree) 6,图 (Graph) 7,堆 (Heap)在计算机科学中,堆是一种特殊的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。 冒泡排序: main() { int i,j,temp; int a[10]; for(i=0;i<10;i++) scanf ("%d,",&a[i]); for(j=0;j<9;j++) { for (i=0;i<9-j;i++) { if (a[i]>a[i+1]) { temp=a[i]; a[i]=a[i+1]; a[i+1]=temp;} } } for(i=0;i<10;i++) printf("%5d,",a[i] ); printf("\n"); } 选择法排序: main() { int a[10],i,j,k,t,n=10; for(i=0;i<10;i++) scanf("%d",&a[i]); for(i=0;i<n-1;i++) /*外循环控制趟数,n个数选n-1趟*/ { k=i; /*假设当前趟的第一个数为最值,记在k中 */ for(j=i+1;j<n;j++) /*从下一个数到最后一个数之间找最值*/ if(a[k]<a[j]) /*若其后有比最值更大的*/ k=j; /*则将其下标记在k中*/ if(k!=i) /*若k不为最初的i值,说明在其后找到比其更大的数*/ { t=a[k]; a[k]=a[i]; a[i]=t; /*则交换最值和当前序列的第一个数*/ } } for(i=0;i<10;i++) printf("%d ",a[i]); } 插入法排序: main() { int a[10],i,j,t; for(i=0;i<10;i++) scanf("%d",&a[i]); for(i=1;i<10;i++) /*外循环控制趟数,n个数从第2个数开始到最后共进行n-1次插入*/ { t=a[i]; /*将待插入数暂存于变量t中*/ for( j=i-1;j>=0 && t>a[j];j-- ) /*在有序序列(下标0 ~ i-1)中寻找插入位置*/ a[j+1]=a[j]; /*若未找到插入位置,则当前元素后移一个位置*/ a[j+1]=t; } for(i=0;i<10;i++) printf("%d ",a[i]); } 快速排序: #include <stdio.h> int quichsort(int *arr, int left, int right); void swap(int *a, int *b); int main() { int arr[] = {9, 2, 1, 4, 3, 15, 11, 6, 7}; int arr_len = sizeof(arr) / sizeof(arr[0]); int i; for (i = 0; i < arr_len; i++) { printf("%d ", arr[i]); } printf("\n"); quicksort(arr, 0, arr_len - 1); for (i = 0; i < arr_len; i++) printf("%d ", arr[i]); printf("\n"); return 0; } int quicksort(int *arr, int left, int right) { int i = left; int j = right; int one_key = arr[left]; if (i >= j) //avoid infinity recursion return 0; while (i != j) { //accomplish one quicksort while (arr[j] > one_key) { j--; } swap(&arr[j], &one_key); while (arr[i] < one_key) { i++; } swap(&arr[i], &one_key); } quicksort(arr, left, i - 1); //accomplish the left remaining quicksort quicksort(arr, j + 1, right); //accomplish the right remaining quicksort } void swap(int *a, int *b) { int tmp; tmp = *a; *a = *b; *b = tmp; } 下面哪种排序法对12354最快--插入排序应该最快,其次是快速排序,归并排序。 a quick sort b.buble sort c.merge sort 快速排序:有一个划分元素,该元素左边的所有元素都小于它,右边的所有元素都大于它。 冒泡排序:依次比较相邻的两个数,将大数放在前面,小数放在后面。即首先比较第1个和第2个数,将大数放前,小数放后。这样一趟之后,最小的数就放在了最后面。 归并排序:将两个有序的数列合并成一个 希尔排序:基本思想:将整个无序序列分割成若干小的子序列分别进行插入排序。序列分割方法:将相隔某个增量h的元素构成一个子序列。在排序过程中,逐次减小这个增量,最后当h减到1时,进行一次插入排序,排序就完成。增量序列一般采用:ht=2t-1,1≤t≤[log2n],其中n为待排序序列的长度。 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% #include<iostream> using namespace std; //函数内的s[0]实际只是一个指向字符串的指针, //没有任何额外的与数组相关的信息,因此sizeof作用于上只将其当指针看,一个指针为4个字节,因此返回4。 void aa(char s[]) { cout<<sizeof(s)/sizeof(s[0])<<endl;//4 } //函数内的s[0]实际只是一个指向字符串的指针, //没有任何额外的与数组相关的信息,因此sizeof作用于上只将其当指针看,一个指针为4个字节,因此返回4。 void bb(char *s) { cout<<sizeof(s)/sizeof(s[0])<<endl;//4 } void main() { char s[]="abc123"; aa(s); bb(s); cout<<sizeof(s)/sizeof(s[0])<<endl;//7 } /*-- 以上代码中的两个sizeof用法有问题吗? 答:函数内的sizeof有问题。 根据语法,sizeof如用于数组,只能测出静态数组的大小,无法检测动态分配的或外部数组大小。 函数外的s是一个静态定义的数组,因此其大小为7,函数内的s实际只是一个指向字符串的指针, 没有任何额外的与数组相关的信息,因此sizeof作用于上只将其当指针看,一个指针为4个字节,因此返回4。 Class Test{int a;static double c};//sizeof(Test)=4. 结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关。 Class test1{ };//sizeof(test1)=1; 没有成员变量的结构或类的大小为1,因为必须保证结构或类的每一个实例在内存中都有唯一的地址。 int func(char s[5]); //sizeof(s)=4 函数的参数在传递的时候系统处理为一个指针 izeof(func("1234"))=4//因为func的返回类型为int,所以相当于求sizeof(int). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% const int i=0; //i是常量,i的值不会被修改 const int *p1=&i; //指针p1所指内容是常量,可以不初始化,指针可以变化 int * const p2; //指针p2是常量,所指内容可修改 const int * const p3=&i; //指针p3是常量,所指内容也是常量 const位于*左侧,指针指向为常量; const位于*右侧,指针本身为常量; ####################################################### 以下代码能够编译通过吗,为什么? unsigned int const size1 = 2; char str1[ size1 ]; unsigned int temp = 0; cin >> temp; unsigned int const size2 = temp; char str2[ size2 ]; 答:str2定义出错,size2非编译器期间常量,而数组定义要求长度必须为编译期常量。 对于编译期常量,编译器常常在编译时就可以折叠开。而对于运行期常量,编译期无法折叠,编译器能做的,只是对所以可能修改它的动做报错。 编译期常量最常见的例子是编译时的常数定义,比如: #define MAX 128 这个 MAX 就是编译期常量, 没有对应的内存空间,在编译时候, 所有的 MAX 都被128这个值代替 const double PI = 3.1415926; 运行期常量的最常见的例子是函数的常量参数(包括常引用,常指针参数)比如: void f(const string& s) {...} 次常见的例子是类的非静态常量成员。 ——这些都是一经初始化,不允许再发生变化的,但其初始值必须到运行时才能知道。 在运行时常量,它的值虽然在运行时初始化后不再发生变化,但问题就在于它的初始值要到编译时才能确定。比如: srand(clock()); const int i = rand(); 虽然i的值在定义并初始化成不会再发生变化(除非你使用一些不符合标准的小技巧),但再聪明的编译器也无法在编译时确定它的值呀。 ######################################################################## ????????????????????????????????????????????????????????????????? int a = *p++; //等价于a = *(p++);即a = *p; p = p + 1; ++的优先级和*相同,右结合 int b = *++p; //等价于b = *(++p); 即p = p + 1; b = *p; *p++是先取出*p的值,然后让p++。。*p++ :先对指针p解引用,就是求*p,然后对指针自增,即p++; (*p)++是先取出*p的值,让这个值++ 。。(*p)++ :先求*p,然后将括号里的当成一个整体,即*p,对*p自增,求*p++; *(P++)是先取出*p的值,让p++ 所以,*p++等价于*(P++) #include <iostream.h> int main(){ char s[] = "012345678", *p = s; cout << *p++ << *(p++) << (*p)++ << *++p << *(++p) <<++*p << ++(*p) << endl; /*p = s; cout << *p++ << endl; cout << *(p++) << endl; cout << (*p)++ << endl; cout << * ++p << endl; cout << *(++p) <<endl; cout << ++*p << endl; cout << ++( *p) <<endl;*/ return 0; } VC++6 DEBUG下分析:cout的运算是从右向左进行的,但最后输出还是从左到右。所以cout << *p++ << *(p++) << (*p)++ << *++p << *(++p) <<++*p << ++(*p) << endl;依次++(*p),++*p,*(++p),*++p,(*p)++,*(p++), *p++ ,最后再反着输出。 1.++(*p):P指向S[0],并把S[0]加1做为表达式的值,所以输出为1,此时S[0]=='1' 2.++*p:P还指向S[0](S[0]现在的值为1),并把S[0]加1做为表达式的值,所以输出为2,此时S[0]=='2' 3.*(++p):p指向S[1],然后取S[1]的值作为表达式的值,输出'1' 4.*++p :P指向S[2],然后取S[2]的值作为表达式的值,输出'2' 5.(*p)++:P还是指向S[2],取S[2]的值作为表达式的值,所以输出'2',然后S[2]的值加1,S[2]==3 6.*(p++):P还是指向S[2](现值为3),取S[2]的值作为表达式的值,所以输出'3',然后P指向S[3] 7.*p++ :P指向S[3],取S[3]的值作为表达式的值,所以输出'3',然后P指向S[4]; 最后反着输出为3322121 ??????????????????????????????????????????????????????????????????????? (1)指针数组: 是数组,但数组中的每个元素都是指针,int *p[5] (2)指向数组的指针: 是个指针,但它指向的是一个数组,int (*p)[5] int a[10];a是数组首地址,也就是a[0]的地址,a+1是数组下一元素的地址,即a[1],&a是对象的首地址,&a+1是下一个对象的地址,即a[10] ------------------apue---------------------------------- {ssize_t pread(int fd, void *buf, size_t count, off_t offset); ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);} read from or write to a file descriptor at a given off-set。The file offset is not changed. 文件系统:(磁盘文件系统+虚拟文件系统)---这里讲的是磁盘文件系统 ext文件系统:ext2,ext3---细节太多,不讲 UFS(ext前身) ntfs FAT---标识文件用文件名或路径。FAT文件系统的本质:静态单链表。(静态即是用数组实现的,数组是静态的),静态单链表的缺陷即单链表的缺陷:1,回不去;2,限制文件大小:惧怕大文件 优点:小,轻量级(优盘,SID卡依然在用FAT) 一个磁盘可分为一个或多个分区,每个分区包含一个文件系统,每个分区都有一个根,unix是把所有的文件组合到一个数组里,这就需要虚拟文件系统。---虚拟文件系统只存在内存里,并不存在于磁盘里。 创建虚拟文件系统:先去读根分区,然后拿根分区做模板,找到根分区之后就能找到/etc/fstab文件,然后把文件mount 根下( vim /etc/fstab )UUID=e1863969-f85d-4683-be65-422665397343 /boot ---读根分区 home swap 真正的虚拟的文件:proc,sys,dev(设备文件,不可能存在于磁盘里) 分区: sb(superblock超级块)__块位图_____inode区________block(块区)____ 最小的文件系统为512字节,一块不可能存放两个文件。不同的文件系统适用于不同的大文件或小文件。 inode区(UNIX标识文件用inode号):文件属性(inode中有stat结构体中所有的成员),文件存储的位置---一个inode里存储了所有的属性和数据以及无关的数据,唯独没有文件名,文件名是专门存放在目录文件内的。每一个目录下都有目录文件 文件是如何找到的:目录也是文件,存在于磁盘中,一个目录其实是一个数组,数组里每个成员至少存在两个东西:文件名name和目录项的inode。一个目录下有多个目录项,目录项也是一个数组:name+inode 例:/aa/bb/cc---根下有一个目录,每一个目录项都存在两个东西:文件名name和inod,先找到aa目录项的inod,再找到aa的块,在aa的目录里找到bb目录项的inode,再找到bb的块,在bb的目录里找到cc目录项的inode,再找到cc的块。 链接: 一个目录的链接数至少是2:.和.. 块位图:每一位表示一个开关,对应于block,用0/1表示是否被占用。以字节为单位,每一块为512字节(半k),一定能整除8 inode位图:对应于inode,用0/1表示是否被占用。 link unlink---系统调用 remove---库函数,是用unlink实现的。 rename 符号链接---只存路径的一个文件 ,符号链接本身是可用的,但是使用成功与否在于指向文件的权限 、 int symlink(const char *oldpath, const char *newpath); ssize_t readlink(const char *path, char *buf, size_t bufsiz); 4096以下的地址全都不可用 系统调用全都是用二进制方法实现的,没有文本操作 系统调用--------------------标准库 文件描述符 FILE *:可操作文件 系统调用和标准库混用的时候,一定要每次都得fflush,但是最好别混用,很容易出错,因为系统调用没有缓冲 标准库是否一定要基于系统调用:不一定,要看是否进入内核,比如strcat 可以根据fp获得fd:fileno() lseek: fseek: ftell:获取当前位置 sizeof 是关键字不是函数 void fun(int b[100]) { printf("%d\n",sizeof(b)); } int *p=NULL; sizeof(p)=4; p指向一个NULL的指针; sizeof(*p)=4; *p是指向指针的第一个数; int a[100]; sizeof(a)=400; a是数组名,表示整个数组的大小; sizeof(a[100])=100; sizeof(&a)=400; &a是指向数组a的地址; fun(a)=4; 数组传递时是传递数组的头指针,所以是int型; p=a;则p+1指向a[1]; p=&a;则p+1指向a[101]; 临时文件 char *tmpnam(char *s)---is dangerous localtime gmtime这两个函数返回的指针很可能指向同一块空间造成覆盖,安全的办法就是使用gmtime_r 当涉及到硬件时一般才说中断,其他时间都叫打断 不可打断的休眠是当条件满足时才开始醒 kill要杀死进程得满足几个条件:不能是init;必须具有权限;进程不能处于不可打断的休眠状态 内核不能随便使用user的空间,要调用函数,就得使用栈 进程: vim /usr/src/kernels/2.6.32-358.el6.x86_64/include/linux/sched.h 进程(并发)的调度是把他们放在队列里,但是事实上并非是这样,而是有优先级的调度 所有进程是穿在一个链表里的,用来关机用的,每个进程都有自己的页表(页表即虚拟地址和物理地址的对应关系),可以保证访问的空间互相不干扰 linux内核里从来都不区分进程和线程,对于内核来说都是一个任务 进程是一个树形结构,有父子关系。父进程,临时父进程(与调试有关) 每个进程都是运行在虚拟机上的,与外界没有什么交互,如果要实现交互,得有一些特殊的操作。 线程:没有父子关系。只有一个主线程。。。 alloca()函数,自动free,分配内存---不是标准函数,有的系统有有的系统没有 goto为什么不能跨函数跳转,函数是记录在栈里的,goto没有记录跳转的位置,因此不能跨函数跳转,而setjmp和longjmp会记录好跳转的线程,因此可以跨函数跳转 setjmp(jmp_buf env);setjmp记录一个栈的位置到env里 void longjmp(jmp_buf env, int val) (ulimit -a) int getrlimit(int resource, struct rlimit *rlim); ----调整软硬限制 ,硬限制跟硬件无关 int setrlimit(int resource, const struct rlimit *rlim); ssh -qTfnN 帐号(yihua@www.embsky.com) -D+端口(7070) autoproxy ssh -D---默认代理 fork()---进程不能凭空创建,只能从原进程复制过来的。父子进程号PPID,子进程号PID---两个进程互相独立。父进程可以有多个子进程 父子进程是异步的,子进程先退出了,子进程的所有资源释放,但是父进程还没有接收子进程释放的资源,子进程就变成zombie进程,需要wait() 父进程先退出,则子进程由init接管---子进程变成了孤儿进程 ps aux |grep a.out wait(&int):等待为子进程收尸,int为子进程的返回值,其取值范围只能为0-255,如果超过了,则会产生溢出 vfork()---保证子进程先执行,子进程先进行exec,子进程结束之后父进程再开始执行----基本上不用,但面试可能会用到 exec() 在main函数里调用return等价于调用exit a.out要执行,就得用到exec函数,exec函数是帮我们去执行一个可执行文件,exec成功执行完毕会返回错误,继而替换当前进程。 ls -hl `which vim` -rwxr-xr-x. 1 root root 1.9M 2月 17 2012 /usr/bin/vim ls -lh `which busybox` -rwxr-xr-x. 1 root root 1022K 4月 6 2012 /sbin/busybox echo $PATH /usr/lib64/qt-3.3/bin:/usr/kerberos/sbin:/usr/kerberos/bin:/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/root/bin 脚本文件: #!/bin/bash bash帮忙打开执行文件 时钟中断应用于调度,时钟中断多久来一次:100~1000,sysconf(_SC_CLK_TCK)---The number of clock ticks per second 信号:信号不是中断,也不是软中断 两个常见的异步:信号或者中断之类的 一个信号只能关联一个函数,形成单射关系,也可以多个信号关联一个函数 man 7 signal SIGINT ctrl+c signo=2 SIGQUIT ctrl+\ signo=3 kill -l可以查看各种信号以及signo 当两个信号都使用时,需用kill杀死进程,ps aux | grep XXX ---kill +进程号 信号的响应从来不是实时的 所谓的阻塞实质上是指休眠,休眠可以分为两类:可打断的休眠,不可打断的休眠 信号中的阻塞和忽略不可捕获 信号会打断阻塞的系统调用,如sleep会被ctrl+c打断 信号的问题:1,信号的重入问题:重入的行为是很危险的,在一个函数未执行完之前又执行了一次这个函数即重入.引发重入:递归函数(主动重入),多任务(多个任务使用同一个资源) 解决重入的办法:不让其重入;加锁;写重入的函数 2,信号的丢失问题是不可解决的 exec对信号的影响??????????????????????????????????????????????????????????????????? 信号阻塞则不会产生回应 在linux上sleep是能安全使用的 sigaction.sa_mask---防止重入,用之前先清空 进程是线程组,线程是基于进程的,一个进程里的线程组是共享资源的。fork()出来的进程是不共享资源的 所有的线程都是平等的,主线程只是工作在main函数里的,main函数也是一个线程。原先unix是没有线程的,一切的定义都是基于进程的 进程结束之后,线程被迫结束。main函数调用pthread_exit,pthread_exit只是结束当前线程,main函数这个线程先结束,但是main函数不会再执行return,其他线程可以继续执行,这个进程还是在进行的,当所有线程执行完毕之后进程才结束。 杀死线程函数:pthread_cancel。pthread_cancel和kill一样会延时 进程之间通过文件进行通信,pipe---则需要两个文件描述符,一个用来读(0),一个用来写(1), pipe是有亲缘关系的进程之间的通信 fifo是没有亲缘关系的进程之间的通信,管道文件的创建两种方式:命令行-mkfifo;函数mkfifo 读写都是阻塞的,必须两个同时打开./send 和 ./recv---管道必须凑齐读写双方 管道只能是一对一或者多对一的通信,不能多对多的通信。消息队列实现多对多,key(由ftok产生,一个路径名和一个项目id就可以产生一个key)相同,则会使用相同的ipc 命令ipcs查看消息队列的msgid和key值,如果没有手动的删掉消息队列,它就会一直保留,ipcrm -q +msgid----手动删除ipc msgctl(msgid,IPC_RMID,NULL)函数也可以删除ipc,但是比较危险,一不小心删除了就会全部出错 消息没有对象接收的时候是可以存放在队列里,等有对象了,即可发送给接收对象 信号量可以用来加锁(进程间的),它本身不是锁,但由于其减只能减到0为止这个特性,故可以当锁来用 信号量可用于不同内存的同步 栅栏同步---fence---是演示semaphore数组最好的办法 信号量是用来同步的,而信号是异步的,信号是会丢失的,而信号量是不会丢失的,信号量是一个数组,当考虑为一个单一的元素时,为整型,可以无限加,但只能减为0为止,因此可以当锁来用,用semop()来进行加减,sem_flg没有加上IPC_NOWAIT ,则是阻塞行为,如果加上了,则是非阻塞行为,在用做锁时sem_flg取SEM_UNDO。IPC_NOWAIT不管是成功还是失败都是马上返回,不会被打断。赋值时可以赋给负值(信号量一定是负数:X),原则上是自然数,事实上允许是负数。 除了加锁之外,提供令牌是semaphore最常用的行为(因为其可以无限加)。 semaphore可以在任意场合加锁 当把信号量考虑为一个数组时,可以对其单个元素进行操作,也可以整体操作。信号量当锁用时将其初始化为1,加锁为加1,解锁为减1;当令牌用时,将信号量初始化为0; semget(key_t key, int nsems, int semflg)---------可以通过semget得到一个数组,nsems是数组的个数 semctl(int semid, int semnum, int cmd,)--- ...-可以对信号量数组进行整体操作,赋值...第二个参数指的是信号量(信号量是数组)的下标,0~n-1,写-1表示忽略某个值,setall时不想胡乱填值,就写-1;这里的值一般要么是0(单个信号量)要么是-1(整体信号量数组) semop(int semid, struct sembuf *sops, unsigned nsops);---第三个参数表示前面要操作的 struct sembuf的个数---semaphore的值跟进程的休眠状态无关,只有当semaphore进行加减操作时,即用semop函数时,sem_flg有没有加上IPC_NOWAIT才对进程有影响。阻塞方式时sem_flg设为0,表示既不是IPC_NOWAIT,也不是SEM_UNDO。。SEM_UNDO不能随便加,只有在加锁解锁时才能用到。加解锁时SEM_UNDO每次记录semop()里的加减,是在退出时内核帮助我们用来自动还原,避免异常终止状态下死锁。 记录锁:---fcntl(fd,F_SETLKW,&l)---struct flock l; 写锁与所有锁都互斥,读锁可以共享(读锁可以同时加多次,而写锁只能加一次) 写锁,读锁,新锁会替代旧锁---覆盖 是pid对某个文件的某些区域加锁---当fork之后,子进程和父进程pid不相同,因此父进程加锁之后子进程并没有加锁,而当exec,pid并没有改变 close文件时,就算多次open,只要有一次close,则进程对文件加的所有锁都释放。不能在同一个进程当中的各个线程使用加锁,线程资源共享,使用同一个pid,因此不同线程之间后加的锁会覆盖原来加的锁 socket (服务器)recv端:accept---newsd,通过sd得到newsd,newsd是新的接口,跟服务器无关,只是一个接口。 (client)send端:connect--sd 一块网卡至少对应两个ip 重用地址就是为了防止在进程结束时,bind还未来得及解除,有可能是有子进程还未退出,也有可能是内核解除bind延迟导致的未来得及解除bind,在下一次再执行该程序时bind失败,getsockopt即可解决这种问题 流式套接字默认协议是tcp,只有流式套接字才有监听模式(listen),报式套接字是没有的。 三次握手,四次分手:发包,反馈确认;反馈确认的确认;... 广播和多播: 路由器:是工作于网络层,通过ip转发 交换机:是工作于链路层,通过 广播风暴 全广播域:255.255.255.255 在192.168.0.*里的广播域:192.168.0.255 绑定本机所有ip:0.0.0.0,相当于写上inadd_any iptables -F --------关闭防火墙命令 service iptables stop ----关闭防火墙命令 ip ro sh----查看路由表: 默认路由: mac地址检查收到的包是否是自己需要接收的,如果不是自己的直接丢弃,如果是则接收。互联网上不一定使用mac。 如何从1发送包到4: 1:(4必须是公网ip): 源:ip1,mac1-->ip1,mac2-->ip1,mac3 目的:ip4,mac2-->ip4,mac3-->ip4,mac4 2:NAT dest source ip4,mac2 ,p4 ip1,mac1,p1 ip4,mac3 ,p4 ip2,mac2,p2 由于安全的问题,有些NAT是不转发包的。 NAT穿透: 3:4是内网:通过服务器中转,转发包;也可以直接把两个通信端的ip告诉服务器,然后直接发包 多播:----只能在局域网里使用 组播和广播属于多播的一部分,但现在说多播一般指的是组播。 组播原则上是不能子互联网上使用的。凡是一对多的必须小心谨慎,很容易引起风暴。 多播组的地址:11100000 0000~ 224.0.0.0/4 属于D类地址 (224.0.2.0,224.0.255.0)这段地址可用,结尾为0的不能用 本地套接字接收方和发送方双方都需要bind,这样才能收到消息,没有bind可以发消息但是不能收到消息 守护进程: 所有日志都应该写在/var/log里,方便系统管理。 日志的权限---找一个类似于中介的东西 syslogd--后面加了一个d表示daemon。syslogd有一个本地套接字,可以将写的日志发给它,这样避免了用户修改了系统原先的log。 openlog--打开日志 syslog--写日志,主要用到syslog closelog---关闭 总结: du -h noteeeeeee---查看大小 UNIX一切皆文件。 (机器码是实际的东西,文件只是一个接口,是一个抽象,机器码和文件是不想提并论的。磁盘不一定是文件。) 文件在UNIX上意味着是操作大多数东西的接口 系统调用:---应用层与内核之间,规定的比较详细,可移植性差 open() close() read() write() lseek() stat() ---------获取文件属性 opendir() closedir() readdir() fcntl()---设置文件的操作方式 ioctl() syscall()----真正的系统调用,通过编号来实现接口,read,write都是用syscall来实现的 int fd; fd=open(); read(fd,buf,BUFSIZE) / write(fd,buf,BUFSIZE);----------都返回实际操作的字符数 close(fd); 标准库:---规定的比较粗糙,跨平台,可移植性好 fopen() fclose() fread() fwrite() fseek() 文本操作:printf()涉及标准输入输出---以上所有操作都是二进制文件,unix里不区分二进制文件和文本文件,文件包含的都是可打印字符,就是文本文件。文本文件是一种特殊的二进制文件,二进制文件是一块一块读取,文本文件是一个一个读取 进程是占有资源的单位(不能说是最小单位,只有进程是能占有资源的,其他是不能占有资源的),资源包括处理器时间,内存,文件等等。事实上不存在虚拟机。 如果进程退出,则所有资源(不包括sysV IPC)都释放,原则上进程间不会共享资源,各自独立。 常用函数: fork() wait()/waitpid() exec函数族 跟权限相关的东西都是进程不能操作的,只能由系统调用,系统调用会切换成高权限模式,跳转到内核态,但是仍然是这个进程,权限是进程身份和文件身份的结合,内核会检查进程的身份,即进程uid,但是一个uid不方便切换,因此使用了三个uid:effct,real,save。设置权限位u+s。exec时,effect uid是谁,则save uid就是谁,并且不会再改变。 环境变量---变量名:变量值 流式套接字想要并发:fork--处理newsd 信号: signal(signo,funcp);--注册信号行为 从宏观角度来看,在进程执行的任意时间点,如果接收到信号signo,就会马上暂停当前执行,转而执行funcp函数,funcp执行完成后回到之前暂停的位置继续执行; 函数返回注册前的信号处理函数。 signal(signo,func1); p=signal(signo,func2); p==func1为真; 信号并非实时响应,当进程从内核态返回到用户态时,才会响应信号。信号可能会丢失,信号本身是不会丢失的,只是信号发送多了,有的信号没来的及响应,没有记录。 由于signal()的各种不确定,建议使用sigaction(); 信号属于进程语义,即信号发给的是进程。如果碰到的是线程,就是另外一回事了。 在多线程环境下,在poxis线程库里规定,只有一个线程能收到信号,不确定具体是哪个线程(不包括时钟相关的信号:alarm,谁调用的alarm,信号就发给谁)。当信号来临时,其他线程通通阻塞信号,然后可以指定一个线程接收到信号,pthread——sigmask可以设置阻塞信号,这种方式是同步的。信号本身是异步的,不知道信号什么时候来,来了才响应,同步是次序是很确定的,都可以预测。 常用的两种线程环境信号处理: 1.所有其他线程阻塞信号,只有一个线程不阻塞信号,就可以确定接到信号的线程。pthread——sigmask();阻塞信号即屏蔽信号,暂时不响应。 2,所有线程阻塞信号,可以让任意一个线程采用同步方式等待信号。sigwait(); 在多线程环境下,进程大致可以看作是线程组; 线程没有父子关系,是完全平等的,main线程只是执行的函数跟其他线程不一样;main函数返回或者调用exit函数会导致进程结束,即所有线程都会结束。 进程才是占有资源的单位,线程是不占有资源的,也就意味着同一个进程的所有线程的资源是共享的(指的主要是内存,处理器在此不讨论)。 pthread_create()--创建线程 pthread_exit()---结束当前线程 pthread_join()---接收线程的返回值,相当于wait(); 线程由于共享资源,更容易引起竞争,要注意同步。 pthread_mutex_*()...; pthread_cond_*(); 高级IO: 1,非阻塞 在调用open()或fcntl()时添加O_NOBLOCK; 2,记录锁 fcntl()的F_GETLK F_SETLK F_SETLKW cmd参数 3,IO多路转接:stack,poll select函数。可以了解以下poll 4,信号驱动IO fcntl(F_SETL,O_ASYNC,) fcntl(F_SETOWN,); 5,存储映射IO mmap() munmap() msync() -----------------------------------------------进程间通信--------------------------------------------------------------- (1),pipe()---匿名管道 (2),fifo()---命名管道,mkfifo() 管道都是文件,匿名管道存在,但是没有path,只能用于有关系的进程。命名管道则有path,可以用于无关的进程。 管道大致上按照stream的方式传输数据。 命名管道必须读写端同时打开,否则就会阻塞。匿名管道无所谓,打开的时候肯定是同时打开。 如果所有写端关闭,读完剩下数据后,read()会返回0,文件结束; 如果所有读端关闭,write()会收到SIGPIPE,管道破裂; (3),sysV IPC : sysV IPC不是基于文件的,跟文件操作有很大区别。 文件是基于inode和path的,而sysV IPC是基于ipcid和key。path可以保证唯一,而key不一定;文件会在进程退出时自动关闭,sysV IPC在进程退出时什么也不做 unlink一个文件,只是删除一个链接(path); 通常IPC_RMID会立即删除IPC对象(shm例外,最后一个使用的进程detach时才删除)----所以一般不建议使用sysV IPC 1,msg struct msg_st----该结构体只是这种相似类型,也可以在结构体中加上成员long type指定source { long type;---用来指定接收方 char data[0]; } int ipcid; ipcid=msgget(); msgctl(ipcid,IPC_RMID);---删除 msgrcv(); msgsnd(); 2,sem semaphore首先是一个数组,每个元素都是整数,可以赋值,取值,加减;原则上可以无限加,减最多只能减到0(if(xx-num < 0),sleep;); 既可以用来加锁,又可以用来分配资源; int ipcid; ipcid = semget(); semctl(ipcid,IPC_RMID);---destroy semop();---进行加减 semctl(ipcid,GETALL / GETVAL /SETALL /SETVAL,); 3,shm int ipcid; ipcid=shmget(); shmctl(ipcid,IPC_RMID); shmat(); shmdt(); -------------------------------------------------------套接字socket---------------------------------------------------------------- (1)网络套接字inet 网络编程要注意的几个问题: a:字节序---大端小端,网络即涉及不同的机器不同的平台,不同的体系结构有不同的字节序。发包时要转成网络序(大端),收包时要转成主机序; b:c内置类型的大小---不要使用内置类型,使用uint32_t一类的类型; c:结构体填充---用于结构体的,紧凑模式。没有标准的方法,不同的编译器有不同的办法,GCC上可以加上__attribute__((__packed__);不同的编译器使用不对齐的紧凑模式,不填充。 协议: tftp协议只能在局域网中使用,不能在互联网上使用,很危险. 域名(例如www.baidu.com)与ip之间应该有对应关系,但是机器上的域名与ip之间没有对应关系,这就需要DNS来解析域名。 地址类型应该是struct sockaddr_in ---man 7 ip int socket_fd /sd; 1,stream流式套接字---像流一样想发多少就发多少,想收多少就收多少。收包的大小可以和发包的大小不一致,流式套接字会整合包的大小 sd=socket(AF_INET,SOCK_STREAM,0);---生成套接字,默认TCP协议 [setsockopt() ];man 7 ip ,man 7 socket ,man 7 tcp; server端 client端(发送端) bind(); [bind();]---中括号表示可有可无,网络的发送端可以不bind,系统会自动bind到一个端口 listen(); connect(); newsd=accept(); read() /write(); close() /shutdown(); 2,dgram---数据报是无序的,不安全的,可能丢包,每个包的长度是固定长度。报式套接字是不需要创建链接的 sd=socket(AF_INET,SOCK_DGRAM,0);--默认UDP协议 [setsockopt() ];man 7 ip ,man 7 socket ,man 7 tcp; server端(接收端) client端(发送端) bind(); [bind();] [connect();] recvfrom() /sendto(); close() /shutdown(); (2)本地套接字unix/local---也分stream和dgram两种方式 地址类型: struct sockaddr_un int sd = socket(AF_UNIX,..); bind()自动创建socket文件; bind()之前,unlink(); 跟网络套接字不同的地方,本地套接字里比如说dgram套接字的发送端必须bind(),因为网络套接字第一次发包时如果不bind,系统会自动bind一个端口,而本地套接字则不会自动绑定; packed套接字: |<------------------SOCK_RAW-------->| |<--- SOCK_DGRAM----->| 以太帧 dest|src| | M |clc 字节数 6 6 2 4 SOCK_DGRAM---允许操作以太帧头里面的东西。以太帧本身是不能操作的 SOCK_RAW--允许操作整个以太帧。 收包首先要检查mac地址,mac不对则将包丢弃.通过mac地址就能看出厂商。只有通过mac地址才能在广播域里发包,而不是ip。 man 7 packet mr_alen不填 BUFSIZE的大小:1500+帧头(dest6+src6+type4),clc收不到的 packet收包:socket--->设为混杂模式-->收包 混杂模式只是在收包时用,发包则不用 packet发包:收包看看是不是我要篡改的包,是的话复制一份然后丢弃,进行篡改 arp欺骗 路由表里有个default可以多路转发---ip ro sh,在广播域里发包,但是只知道ip而不知道mac是没法发包的。 设置路由器:限速:广播发包,告诉所有电脑我是网关,发包之前要通过一个arp协议 0 ---------------------------------mysql--------------------------- 数据库:mysql,oracle(太贵),DB2,access(很小),sqlserver, mysql-5.1.52----数据库的管理软件 mysql-server----,服务器,是一个daemon mysql-devel mysql-libs mysql---指的是客户端 mysql -h ip(192.168.0.90) -u username -p----------------------连接 quit,\q-----------退出 service mysqld start-----启动服务 ps aux |grep mysql mysqladmin -uroot password 'mysql123'----第一次修改密码 mysql -u root -p-----登录 mysqladmin -uroot password 'mysql123' -p ,输入原来密码,再输入mysql -u root -p,输入新密码-----第二次修改密码 忘记密码: rm -rf /var/lib/mysql/mysql mysql> show databases;----注意加分号,加分号表示一句结束,逗号表示分隔 新建一个我们自己的数据库: create database apue(数据库名) ;----创建一个名为apue的数据库 drop database apue;----------------删除数据库 show databases;--------------------查看有哪些数据库 use apue;--------------------------选择数据库 select database();-----------------查看当前数据库 表操作: show tables;-----------------------数据库里存放的是表 mysql> create table base1 ( ------创建表:create table 表 -> name varchar(32),-----------字段1 类型 [约束条件], -> sex enum('male','female'),--字段2 类型 [约束条件] -> age int, ... -> id int----------------------注意最后一个字段是没有逗号的 -> ); drop table base1;------------------删除表 show tables------------------------查看有哪些表 desc base1;-----------------------查看表的属性 记录:添加,删除,修改,查找 insert into (表名)base1 values('zhanghuan','male',18,1);-----在某表里添加信息:insert into 表名 values(值1,值2,值3 ...);value必须与创建表时一一对应 insert into base1(name,age) valus('wangkai',20); select * from base1;---------------查找表base1里所有的信息:select 字段1,字段2,...from 表名 where 条件; select name,sex from base1; select * from base1 where name='zhanghuan'; select * from base1 where age=20 and sex='male'; update base1 set age=19 where name ='zhanghuan';--------修改 select * from apue.base1;-------------------查看别的库 delete from base1 where name='zhanghuan';---删除:delete from 表名 where 条件 delete from base1 where sex=NULL and id=NULL;------写NULL是删不掉的; GNU里的make才支持的语法: Makefile: CFLAGS :=$(shell mysql_config --include) LDFLAGS :=$(shell mysql_config --libs) %:%.c gcc -o $@ $< $(CFLAGS) $(LDFLAGS) clean: rm -f *~ $.o mysql_library_init; mysql_init; mysql_real_connect; mysql_query(mysql,"insert into apue.base1 values('lala','male',25,10)"); mysql_close(mysql); mysql_library_end; 3.Windows程序的入口是哪里?写出Windows消息机制的流程。 Windows程序的入口是WinMain函数 消息机制:系统将会维护一个或多个消息队列,所有产生的消息都会被放入或是插入队列中。系统会在队列中取出每一条消息,根据消息的接收句柄而将该消息发送给拥有该窗口的程序的消息循环。每一个运行的程序都有自己的消息循环,在循环中得到属于自己的消息并根据接收窗口的句柄调用相应的窗口过程。而在没有消息时消息循环就将控制权交给系统。 4.如何定义和实现一个类的成员函数为回调函数? 所谓的回调函数,就是预先在系统的对函数进行注册,让系统知道这个函数的存在,以后,当某个事件发生时,再调用这个函数对事件进行响应。 定义一个类的成员函数时在该函数前加CALLBACK即将其定义为回调函数,函数的实现和普通成员函数没有区别 简而言之,回调函数就是一个通过函数指针调用的函数。如果你把函数的指针作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。 Callback最本质的特征包括两点:注册和触发 5,C++里面是不是所有的动作都是main()引起的?如果不是,请举例。 答:不是,比如中断引起的中断处理不是直接由main()引起的,而是由外部事件引起的。 比如全局变量的初始化,就不是由main函数引起的! 系统会为某个启动的程序分配地址空间,创建进程和主线程,并为main()指供参数(如果有的话),然后才转到main()执行 6.C++里面如何声明const void f(void)函数为C程序中的库函数? 答:在该函数前添加extern “C”声明 被extern "C"修饰的变量和函数是按照C语言方式编译和连接的; 未加extern “C”声明时的编译方式 首先看看C++中对类似C的函数是怎样编译的。 作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为: void foo( int x, int y ); 该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。 _foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。 同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与 函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。 8. 内联函数在编译时是否做参数类型检查? 答:做类型检查,因为内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来代替。 内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内。内联函数从源代码层看,有函数的结构,而在编译后,却不具备函数的性质。内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。编译时,类似宏替换,使用函数体替换调用处的函数名。一般在代码中用inline修饰,但是能否形成内联函数,需要看编译器对该函数定义的具体处理。 动机:内联扩展是用来消除函数调用时的时间开销。它通常用于频繁执行的函数。 一个小内存空间的函数非常受益,它主要的作用是解决程序的运行效率。。 如果没有内联函数,编译器可以决定哪些函数内联 。 程序员很少或没有控制哪些只能是内联的,哪些不是。 给这种控制程度,作用是程序员可以选择内联的特定应用 。 使用内联函数的时候要注意: 1.递归函数不能定义为内联函数 2.内联函数一般适合于不存在while和switch等复杂的结构且只有1~5条语句的小函数上,否则编译系统将该函数视为普通函数。 3.内联函数只能先定义后使用,否则编译系统也会把它认为是普通函数。 4.对内联函数不能进行异常的接口声明。 内联函数的功能和预处理宏的功能相似。内联函数和宏的区别在于,宏定义没有语法检查,内敛函数有语法检查,更不会出错!宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。 内联函数必须是和函数体声明在一起,才有效。像这样的申明Inline Tablefunction(int I)是没有效果的,编译器只是把函数作为普通的函数声明,我们必须定义函数体。Inline tablefunction(int I) {return I*I};这样我们才算定义了一个内联函数。 内联函数具有一般函数的特性,它与一般函数所不同之处只在于函数调用的处理。一般函数进行调用时,要将程序执行权转到被调用函数中,然后再返回到调用它的函数中;而内联函数在调用时,是将调用表达式用内联函数体来替换。在使用内联函数时,应注意如下几点: 1.在内联函数内不允许用循环语句和开关语句。 如果内联函数有这些语句,则编译将该函数视同普通函数那样产生函数调用代码,递归函数(自己调用自己的函数)是不能被用来做内联函数的。内联函数只适合于只有1~5行的小函数。对一个含有许多语句的大函数,函数调用和返回的开销相对来说微不足道,所以也没有必要用内联函数实现。 2.内联函数的定义必须出现在内联函数第一次被调用之前。 3.本栏目讲到的类结构中所有在类说明内部定义的函数是内联函数。 6.含参数的宏与函数的优缺点 宏: 优点:在预处理阶段完成,不占用编译时间,同时,省去了函数调用的开销,运行效率高 缺点:不进行类型检查,多次宏替换会导致代码体积变大,而且由于宏本质上是字符串替换,故可能会由于一些参数的副作用导致得出错误的结果。 函数: 优点:没有带参数宏可能导致的副作用,进行类型检查,计算的正确性更有保证。 缺点:函数调用需要参数、返回地址等的入栈、出栈开销,效率没有带参数宏高 PS:宏与内联函数的区别 内联函数和宏都是在程序出现的地方展开,内联函数不是通过函数调用实现的,是在调用该函数的程序处将它展开(在编译期间完成的);宏同样是; 不同的是:内联函数可以在编译期间完成诸如类型检测,语句是否正确等编译功能;宏就不具有这样的功能,而且宏展开的时间和内联函数也是不同的(在运行期间展开 9, 三个float:a,b,c 问值 (a+b)+c==(b+a)+c;(a+b)+c==(a+c)+b 第一个成立 第二个不成立。也就是说 浮点运算可交换,但是不满足结合律 1 #i nclude “filename.h”和#i nclude <filename.h>的区别? #i nclude “filename.h”表明该文件是用户提供的头文件,查找该文件时从当前文件目录开始;#i nclude <filename.h>表明这个文件是一个工程或标准头文件,查找过程会检查预定义的目录。 2 头文件的作用是什么? 答:一、通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。 二、头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。 3 C++函数中值的传递方式有哪几种? 答:C++函数的三种传递方式为:值传递、指针传递和引用传递。 4 内存的分配方式的分配方式有几种? 答:一、从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量。 二、在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 三、从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。 5 实现双向链表删除一个节点P,在节点P后插入一个节点,写出这两个函数; 答:双向链表删除一个节点P template<class type> void list<type>::delnode(int p) { int k=1; listnode<type> *ptr,*t; ptr=first; while(ptr->next!=NULL&&k!=p) { ptr=ptr->next; k++; } t=ptr->next; cout<<"你已经将数据项 "<<t->data<<"删除"<<endl; ptr->next=ptr->next->next; length--; delete t; } 在节点P后插入一个节点: template<class type> bool list<type>::insert(type t,int p) { listnode<type> *ptr; ptr=first; int k=1; while(ptr!=NULL&&k<p) { ptr=ptr->next; k++; } if(ptr==NULL&&k!=p) return false; else { listnode<type> *tp; tp=new listnode<type>; tp->data=t; tp->next=ptr->next; ptr->next=tp; length++; return true; } } 9 请问C++的类和C里面的struct有什么区别? struct成员默认访问权限为public,而class成员默认访问权限为private 20 请讲一讲析构函数和虚函数的用法和作用? 析构函数是在对象生存期结束时自动调用的函数,用来释放在构造函数分配的内存。 虚函数是指被关键字virtual说明的函数,作用是使用C++语言的多态特性 21 全局变量和局部变量有什么区别?实怎么实现的?操作系统和编译器是怎么知道的 1) 全局变量的作用用这个程序块,而局部变量作用于当前函数 2) 前者在内存中分配在全局数据区,后者分配在栈区 3) 生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁,局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在 4) 使用方式不同:通过声明后全局变量程序的各个部分都可以用到,局部变量只能在局部使用 22,有N个大小不等的自然数(1--N),请将它们由小到大排序.要求程序算法:时间复杂度为O(n),空间复杂度为O(1)。 #include<stdio.h> int main() { int a[9]={9,8,2,4,3,5,6,7,1}; int i,tmp; for(i=0;i<9;i++) { while(a[i]!=i+1) { tmp=a[i]; a[i]=a[a[i]-1]; a[tmp-1]=tmp; } } for(i=0;i<9;i++) printf("%d ",a[i]); return 0; } 5. 堆与栈的去区别 A. 申请方式不同 Stack由系统自动分配,而heap需要程序员自己申请,并指明大小。 B. 申请后系统的响应不同 Stack:只要栈的剩余空间大于申请空间,系统就为程序提供内存,否则将抛出栈溢出异常 Heap:当系统收到程序申请时,先遍历操作系统中记录空闲内存地址的链表,寻找第一个大于所申请空间的堆结点,然后将该结点从空间结点链表中删除,并将该结点的空间分配给程序。另外,大多数系统还会在这块内存空间中的首地址处记录本次分配的大小,以便于delete语句正确释放空间。而且,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动将多余的那部分重新放入空闲链表。 C. 申请大小限制的不同 Stack:在windows下,栈的大小是2M(也可能是1M它是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 D. 申请效率的比较: 栈由系统自动分配,速度较快。但程序员是无法控制的。 堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。 另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。 E. 堆和栈中的存储内容 栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。 堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排 如何定义和实现一个类的成员函数为回调函数 要定义和实现一个类的成员函数为回调函数需要做三件事: a. 声明; b. 定义; c. 设置触发条件,就是在你的函数中把你的回调函数名作为一个参数,以便系统调用 如: 一、声明回调函数类型 typedef void (*FunPtr)(void); 二、定义回调函数 class A { public: A(); static void callBackFun(void) //回调函数,必须声明为static { cout<<"callBackFun"<<endl; } virtual ~A(); }; 三、设置触发条件 void Funtype(FunPtr p) { p(); } void main(void) { Funtype(A::callBackFun); } ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-- 对于GetMemory()函数,我想说“GetMemory你大爷的”。 NO1 void GetMemory(char *p) { p=(char *)malloc(100); } void Test() { char * str=NULL; GetMemory(str); strcpy(str,"Hello world"); printf(str); } 实质:GetMemory(str)在调用时会生成一个_str与str指向同一个数,这是因为C语言中函数传递形参不改变实参的内容,但是指针指向的内容是相同的,因此可以用指针控制数据。题中的GetMemory(str),实质是对_str的操作,并没有对str操作,函数结束后_str撤销,因此不会产生新的内存空间,str仍然是一个空指针。 分析:程序崩溃。因为GetMemory 并不能传递动态内存,Test 函数中的 str 一直都是 NULL。strcpy(str, "hello world"); 传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完char *str = NULL; 应该改成: char *GetMemory(){ char *p=(char *)malloc(100); return p; } void Test(void){ char *str=NULL; str=GetMemory(){ strcpy(str,"hello world"); printf(str); } NO2 void GetMemory(char **p, int num) { *p = (char *)malloc(num); } void Test(void) { char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); } 答:输出“hello” NO3 char *GetMemory() { char p[]="Hello World"; return p; } void Test() { char * str=NULL; str=GetMemory(); printf(str); } 实质:可能是乱码。当一个函数调用结束后会释放内存空间,释放它所有变量所占用的空间,所以数组空间被释放掉了,也就是说str所指向的内容不确定是什么东西。但是返回的指针指向的地址是一定的。 NO4 char *GetMemory() { Return “hello world”; } void Test() { char * str=NULL; str=GetMemory(); printf(str); } 实质:本例打印hello world,因为返回常量区,而且并没有修改过。在上一个例子中不一定能打印hello world,因为指向的是栈区。 NO5 void GetMemory(char **p,int num) { *p=(char *)malloc(num); } void Test() { char * str=NULL; GetMemory(&str,100); strcpy(str,"Hello"); printf(str); } 可以正确的打印Hello但是内存泄露了,在GetMemory()中使用了malloc申请内存,但是在最后却没有对申请的内存做任何处理,因此可能导致内存的泄露,非常危险。 NO6 void Test() { char *str=(char *)malloc(100); strcpy(str,"Hello"); free(str); if (str!=NULL) { strcpy(str,"World"); printf(str); } } 输出“world”.申请空间,拷贝字符串,释放空间,前三步操作都没有问题,到了if语句里的判断条件开始出错了。篡改动态内存区的内容,后果难以预料,非常危险。 因为free(str);之后,str 成为野指针,if(str != NULL)语句不起作用。 —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 4. 编写strcat函数 已知strcat函数的原型是char *strcat (char *strDest, const char *strSrc); 其中strDest 是目的字符串,strSrc 是源字符串。 (1)不调用C++/C 的字符串库函数,请编写函数 strcat 答:VC源码: char * __cdecl strcat (char * dst, const char * src) { char * cp = dst; while( *cp ) cp++; /* find end of dst */ while( *cp++ = *src++ ) ; /* Copy src to end of dst */ return( dst ); /* return dst */ } (2)strcat能把strSrc 的内容连接到strDest,为什么还要char * 类型的返回值? 答:方便赋值给其他变量 5. 程序什么时候应该使用线程,什么时候单线程效率高 (1) 耗时的操作使用线程,提高应用程序响应 (2) 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求。 (3) 多CPU系统中,使用线程提高CPU利用率 (4) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独 立的运行部分,这样的程序会利于理解和修改。 其他情况都使用单线程。 7. 关于内存对齐的问题以及sizof()的输出 答:编译器自动对齐的原因:为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。 10. 函数模板与类模板有什么区别? 答:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。 使用类模板可以使用户为类定义一种模式,使得类中的某些数据成员、某些成员函数的参数、某些成员函数的返回值能取任意类型。类模板是对一批仅仅成员数据类型不同的类的抽象,程序员只要为这一批类所组成的整个类家族创建一个类模板,给出一套程序代码,就可以用来生成多种具体的类,(这类可以看作是类模板的实例),从而大大提高编程的效率。 定义类模板的一般形式是: template <类型名 参数名1,类型名参数名2,…> class 类名 { 类声明体 }; 例如,template <class T> class Smemory {… public: void mput(T x); … } 表示定义一个名为Smemory的类模板,其中带类型参数T 在类模板的外部定义类成员函数的一般形式是: template <类型名 参数名1,类型名参数名2,…> 函数返回值类型 类名<参数名 1 参数名 2,…>::成员函数名(形参表) { 函数体 } 例如:template <class T> void Smemory<T>::mput(T x) {…} 表示定义一个类模板Smemory的成员函数,函数名为mput,形参x的类型是T,函数无返回值。 类模板是一个类家族的抽象,它只是对类的描述,编译程序不为类模板(包括成员函数定义)创建程序代码,但是通过对类模板的实例化可以生成一个具体的类以及该具体类的对象。 与函数模板不同的是:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定 其实例化的一般形式是: 类名 <数据类型 1(或数据),数据类型 2(或数据)…> 对象名 例如,Smemory<int> mol; 表示将类模板Smemory的类型参数T全部替换成int 型,从而创建一个具体的类,并生成该具体类的一个对象mol。 12. Linux有内核级线程么。 答:线程通常被定义为一个进程中代码的不同执行路线。从实现方式上划分,线程有两种类型:“用户级线程”和“内核级线程”。 用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。这种线程甚至在象 DOS 这样的操作系统中也可实现,但线程的调度需要用户程序完成,这有些类似 Windows 3.x 的协作式多任务。另外一种则需要内核的参与,由内核完成线程的调度。其依赖于操作系统核心,由内核的内部需求进行创建和撤销,这两种模型各有其好处和缺点。用户线程不需要额外的内核开支,并且用户态线程的实现方式可以被定制或修改以适应特殊应用的要求,但是当一个线程因 I/O 而处于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会;而内核线程则没有各个限制,有利于发挥多处理器的并发优势,但却占用了更多的系统开支。 Windows NT和OS/2支持内核线程。Linux 支持内核级的多线程 13. 使用线程是如何防止出现大的波峰。 答:意思是如何防止同时产生大量的线程,方法是使用线程池,线程池具有可以同时提高调度效率和限制资源使用的好处,线程池中的线程达到最大数时,其他线程就会排队等候。 14. 写出判断ABCD四个表达式是否正确, 若正确, 写出经过表达式中a的值 int a = 4; (A) a += (a++); (B) a += (++a); (C) (a++) += a; (D) (++a) += (a++); a = ? 答:C错误,左侧不是一个有效变量,不能赋值,可改为(++a) += a; 改后答案依次为9,10,10,11 首先先说说为什么(a++)不能做左值而(++a)可以:a++的运算结果并不是a这个变量,而是一个临时变量,其值为a的值,所以你无法进行左值运算。a++的操作就不能作左值,它返回一个临时变量。++a可以是左值,它返回a被加1后的自己。 19. 如何引用一个已经定义过的全局变量? 答:extern 可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个在头文件中声明的全局变理,假定你将那个变写错了,那么在编译期间会报错,如果你用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期间报错。 20. 全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么? 答:可以,在不同的C文件中以static形式来声明同名全局变量。 可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错。 #include 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 6.对于函数参数列表中的以数组类型书写的形式参数,编译器把其解释为普通的指针类型,如对于void func(char sa[100],int ia[20],char *p) 则sa的类型为char*,ia的类型为int*,p的类型为char*. Func ( char str[100] )函数中数组名作为函数形参时,在函数体内,数组名失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。 数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不能被修改; char str[10]; str++; //编译出错,提示str不是左值 7.根据上面的总结,来实战一下: 对于char str[] = "abcdef";就有sizeof(str) == 7,因为str的类型是char[7], 也有sizeof("abcdef") == 7,因为"abcdef"的类型是const char[7]。 对于char *ptr = "abcdef";就有sizeof(ptr) == 4,因为ptr的类型是char*。 对于char str2[10] = "abcdef";就有sizeof(str2) == 10,因为str2的类型是char[10]。 对于void func(char sa[100],int ia[20],char *p); 就有sizeof(sa) == sizeof(ia) == sizeof(p) == 4, 因为sa的类型是char*,ia的类型是int*,p的类型是char*。 8, char* ss = "0123456789"; sizeof(ss) 结果 4 ===》ss是指向字符串常量的字符指针 sizeof(*ss) 结果 1 ===》*ss是第一个字符,字符占1个字节 char ss[] = "0123456789"; sizeof(ss) 结果 11 ===》ss是数组,计算到\0位置,因此是10+1 sizeof(*ss) 结果 1 ===》*ss是第一个字符 char ss[100] = "0123456789"; sizeof(ss) 结果是100 ===》ss表示在内存中的大小 100×1 strlen(ss) 结果是10 ===》strlen是个函数内部实现是用一个循环计算到\0为止之前 int ss[100] = "0123456789"; sizeof(ss) 结果 400 ===》ss表示再内存中的大小 100×4,32位机种int整形数据占4个字节 strlen(ss) 错误 ===》strlen的参数只能是char* 且必须是以''\0''结尾的 char q[]="abc"; char p[]="a\n"; sizeof(q),sizeof(p),strlen(q),strlen(p); 结果是 4 3 3 2 1.static有什么用途?(请至少说明两种) 1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。 2) 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。 3) 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用 2.引用与指针有什么区别? 1) 引用必须被初始化,指针不必。 2) 引用初始化以后不能被改变,指针可以改变所指的对象。 3) 不存在指向空值的引用,但是存在指向空值的指针。 3.描述实时系统的基本特性 在特定时间内完成特定的任务,实时性与可靠性。 5.什么是平衡二叉树? 左右子树都是平衡二叉树 且左右子树的深度差值的绝对值不大于1。 6.堆栈溢出一般是由什么原因导致的? 没有回收垃圾资源。 7.什么函数不能声明为虚函数? constructor函数不能声明为虚函数。 8.冒泡排序算法的时间复杂度是什么? 时间复杂度是O(n^2)。 9.写出float x 与“零值”比较的if语句。 if(x>0.000001&&x<-0.000001) 2.用两个栈实现一个队列的功能?要求给出算法和思路! {队列(queue)在计算机科学中,是一种先进先出的线性表。它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。} 设2个栈为A,B, 一开始均为空. 入队: 将新元素push入栈A; 出队: (1)判断栈B是否为空; (2)如果不为空,则将栈A中所有元素依次pop出并push到栈B; (3)将栈B的栈顶元素pop出; 这样实现的队列入队和出队的平摊复杂度都还是O(1) 1.进程和线程的差别。 线程是指进程内的一个执行单元,也是进程内的可调度实体. 与进程的区别: (1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位 (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行 (3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源. (4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。 9.给两个变量,如何找出一个带环单链表中是什么地方出现环的? 一个递增一,一个递增二,他们指向同一个接点时就是环出现的地方 10.Internet采用哪种网络协议?该协议的主要层次结构? Tcp/Ip协议 主要层次结构为: 应用层/传输层/网络层/数据链路层/物理层。 3. 对象都具有的二方面特征是什么?分别是什么含义? 答:对象都具有的特征是:静态特征和动态特征。 静态特征是指能描述对象的一些属性,动态特征是指对象表现出来的行为 4. 在头文件中进行类的声明,在对应的实现文件中进行类的定义有什么意义? 答:这样可以提高编译效率,因为分开的话只需要编译一次生成对应的.obj文件后,再次应用该类的地方,这个类就不会被再次编译,从而大大提高了效率。 5. 在类的内部定义成员函数的函数体,这种函数会具备那种属性? 答:这种函数会自动为内联函数,这种函数在函数调用的地方在编译阶段都会进行代码替换。 6. 成员函数通过什么来区分不同对象的成员数据?为什么它能够区分? 答:通过this指针来区分的,因为它指向的是对象的首地址。 7. C++编译器自动为类产生的四个缺省函数是什么? 答:默认构造函数,拷贝构造函数,析构函数,赋值函数。 就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。 拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。 很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值 浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,解决办法就是使用“深拷贝”。 防止默认拷贝发生:通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。 10,拷贝构造函数和赋值构造函数最大的不同在于: 拷贝构造是确确实实构造一个新的对象,并给新对象的私有成员赋上参数对象的私有成员的值,新构造的对象和参数对象地址是不一样的,所以如果该类中有一个私有成员是指向堆中某一块内存,如果仅仅对该私有成员进行浅拷贝,那么会出现多个指针指向堆中同一块内存,这是会出现问题,如果那块内存被释放了,就会出现其他指针指向一块被释放的内存,出现未定义的值的问题,如果深拷贝,就不会出现问题,因为深拷贝,不会出现指向堆中同一块内存的问题,因为每一次拷贝,都会开辟新的内存供对象存放其值。 但是赋值构造函数是将一个参数对象中私有成员赋给一个已经在内存中占据内存的对象的私有成员,赋值构造函数被赋值的对象必须已经在内存中,否则调用的将是拷贝构造函数,当然赋值构造函数也有深拷贝和浅拷贝的问题。当然赋值构造函数必须能够处理自我赋值的问题,因为自我赋值会出现指针指向一个已经释放的内存。还有赋值构造函数必须注意它的函数原型,参数必须是引用类型,返回值也必须是引用类型,否则在传参和返回的时候都会再次调用一次拷贝构造函数。 1,拷贝构造函数里能调用private成员变量吗: 答:拷贝构造函数其时就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。 2. 以下函数哪个是拷贝构造函数,为什么? X::X(const X&); 拷贝构造函数 X::X(X); X::X(X&, int a=1); 拷贝构造函数 X::X(X&, int a=1, int b=2); 拷贝构造函数 解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一: a) X& b) const X& c) volatile X& d) const volatile X& 且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数. 3. 一个类中可以存在多于一个的拷贝构造函数吗? 答:类中可以存在超过一个拷贝构造函数。 class X { public: X(const X&); // const 的拷贝构造 X(X&); // 非const的拷贝构造 }; 注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化. class X { public: X(); X(X&); }; const X cx; X x = cx; // error 如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。 这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。 10. 什么时候必须重写拷贝构造函数? 答:当构造函数涉及到动态存储分配空间时,要自己写拷贝构造函数,并且要深拷贝。 11. 构造函数的调用顺序是什么? 答:1.先调用基类构造函数; 2.按声明顺序初始化数据成员; 3.最后调用自己的构造函数。 13. 什么是常对象? 答:常对象是指在任何场合都不能对其成员的值进行修改的对象。 7.C++中为什么用模板类。 答:(1)可用来创建动态增长和减小的数据结构 (2)它是类型无关的,因此具有很高的可复用性。 (3)它在编译时而不是运行时检查数据类型,保证了类型安全 (4)它是平台无关的,可移植性 (5)可用于基本数据类型 8.CSingleLock是干什么的。 答:同步多个线程对一个数据类的同时访问 22.TCP/IP 建立连接的过程?(3-way shake) 答:在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状 态,等待服务器确认; 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个 SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态; 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1) ,此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。 24.触发器怎么工作的? 答:触发器主要是通过事件进行触发而被执行的,当对某一表进行诸如UPDATE、 INSERT、DELETE 这些操作时,数据库就会自动执行触发器所定义的SQL 语句,从而确保对数据的处理必须符合由这些SQL 语句所定义的规则。 25.winsock建立连接的主要实现步骤? 答:服务器端:socker()建立套接字,绑定(bind)并监听(listen),用accept() 等待客户端连接。 客户端:socker()建立套接字,连接(connect)服务器,连接上后使用send()和recv( ),在套接字上写读数据,直至数据交换完毕,closesocket()关闭套接字。 服务器端:accept()发现有客户端连接,建立一个新的套接字,自身重新开始等待连 接。该新产生的套接字使用send()和recv()写读数据,直至数据交换完毕,closesock et()关闭套接字。 8,在C++中,下面三种对象需要调用拷贝构造函数! (1)对象以值传递的方式传入函数参数 (2)对象以值传递的方式从函数返回 (3)对象需要通过另外一个对象进行初始化; 如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。 9. 构造函数与普通函数相比在形式上有什么不同?(构造函数的作用,它的声明形式来分析) 答:构造函数是类的一种特殊成员函数,一般情况下,它是专门用来初始化对象成员变量的。构造函数的名字必须与类名相同,它不具有任何类型,不返回任何值。 25.winsock建立连接的主要实现步骤? 答:服务器端:socker()建立套接字,绑定(bind)并监听(listen),用accept() 等待客户端连接。 客户端:socker()建立套接字,连接(connect)服务器,连接上后使用send()和recv( ),在套接字上写读数据,直至数据交换完毕,closesocket()关闭套接字。 服务器端:accept()发现有客户端连接,建立一个新的套接字,自身重新开始等待连 接。该新产生的套接字使用send()和recv()写读数据,直至数据交换完毕,closesock et()关闭套接字。 27.IP组播有那些好处? 答:Internet上产生的许多新的应用,特别是高带宽的多媒体应用,带来了带宽的急剧 消耗和网络拥挤问题。组播是一种允许一个或多个发送者(组播源)发送单一的数据包 到多个接收者(一次的,同时的)的网络技术。组播可以大大的节省网络带宽,因为无 论有多少个目标地址,在整个网络的任何一条链路上只传送单一的数据包。所以说组播 技术的核心就是针对如何节约网络资源的前提下保证服务质量。 28,,改错:试题1: Code Void test2() { char string[10], str1[10]; for(I=0; I<10;I++) { str1[i] ='a'; } strcpy(string, str1); } 如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string, str1)调用使得从str1内存起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分 函数原型:char *strcpy(char *strDest, const char *strSrc); 试题2: Code Void test3(char* str1) { char string[10]; if(strlen(str1) <= 10) { strcpy(string, str1); } } if(strlen(str1) <= 10)应改为if(strlen(str1) < 10),因为strlen的结果未统计'\0'所占用的1个字节 编写一个标准strcpy函数 char * strcpy( char *strDest, const char *strSrc ) { assert( (strDest != NULL) && (strSrc != NULL) ); char *address = strDest; while( (*strDest++ = * strSrc++) != '\0' ); return address; } 写出一个10分的strlen函数 int strlen( const char *str ) //输入参数const { assert( strt != NULL ); //断言字符串地址非0 int len; while( (*str++) != '\0' ) { len++; } return len; } 29,写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个 #define MIN(A,B) ((A) <= (B) ? (A) : (B)) 满分 #define MIN(A,B) (A) <= (B) ? (A) : (B) 0分 #define MIN(A,B) (A <= B ? A : B ) V 0分; #define MIN(A,B) ((A) <= (B) ? (A) : (B)); 0分 30,编写一个函数,作用是把一个char组成的字符串循环右移n个。比如原来是“abcdefghi”如果n=2,移位后应该是“hiabcdefgh” void LoopMove ( char *pStr, int steps ) { int n = strlen( pStr ) - steps; char tmp[MAX_LEN]; strcpy ( tmp, pStr + n ); strcpy ( tmp + steps, pStr); *( tmp + strlen ( pStr ) ) = '\0'; strcpy( pStr, tmp ); } ___________________________________________________________________________________________________________________________________-- 试题7:编写类String的构造函数、析构函数和赋值函数,已知类String的原型为: Code class String { public: String(const char *str = NULL); // 普通构造函数 String(const String &other); // 拷贝构造函数 ~ String(void); // 析构函数 String & operate =(const String &other); // 赋值函数 private: char *m_data; // 用于保存字符串 }; 解答: Code //普通构造函数 String::String(const char *str) { if(str==NULL) { m_data = new char[1]; // 得分点:对空字符串自动申请存放结束标志'\0'的空 //加分点:对m_data加NULL 判断 *m_data = '\0'; } else { int length = strlen(str); m_data = new char[length+1]; // 若能加 NULL 判断则更好 strcpy(m_data, str); } } // String的析构函数 String::~String(void) { delete [] m_data; // 或delete m_data; } //拷贝构造函数 String::String(const String &other) // 得分点:输入参数为const型 { int length = strlen(other.m_data); m_data = new char[length+1]; //加分点:对m_data加NULL 判断 strcpy(m_data, other.m_data); } //赋值函数 String & String::operate =(const String &other) // 得分点:输入参数为const型 { if(this == &other) //得分点:检查自赋值 return *this; delete [] m_data; //得分点:释放原有的内存资源 int length = strlen( other.m_data ); m_data = new char[length+1]; //加分点:对m_data加NULL 判断 strcpy( m_data, other.m_data ); return *this; //得分点:返回本对象的引用 } 剖析: 能够准确无误地编写出String类的构造函数、拷贝构造函数、赋值函数和析构函数的面试者至少已经具备了C++基本功的60%以上! 在这个类中包括了指针类成员变量m_data,当类中包括指针类成员变量时,一定要重载其拷贝构造函数、赋值函数和析构函数,这既是对C++程序员的基本要求,也是《Effective C++》中特别强调的条款。仔细学习这个类,特别注意加注释的得分点和加分点的意义,这样就具备了60%以上的C++基本功! 14. 静态函数存在的意义? 答:静态私有成员在类外不能被访问,可通过类的静态成员函数来访问; 当类的构造函数是私有的时,不像普通类那样实例化自己,只能通过静态成员函数来调用构造函数 15,static关键字至少有下列n个作用: (1) 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值; (2) 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问; (3) 在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内; (4) 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝; (5) 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。 18. 不允许重载的5个运算符是哪些? 答:1. *(成员指针访问运算符号); 2. ::域运算符; 3. sizeof 长度运算符号; 4. ?:条件运算符号; 5. .(成员访问符)。 19,不使用库函数,编写函数int strcmp(char *source, char *dest). 答案:一 int strcmp(char *source, char *dest) { assert((source!=NULL)&&(dest!=NULL)); int i,j; for(i=0; source[i]==dest[i]; i++) { if(source[i]=='\0' && dest[i]=='\0') return 0; else return -1; } } 答案:二、 int strcmp(char *source, char *dest) { while ( (*source != '\0') && (*source == *dest)) { source++; dest++; } return ( (*source) - (*dest) ) ? -1 : 0; } 1.是不是一个父类写了一个virtual 函数,如果子类覆盖它的函数不加virtual ,也能实现多态? virtual修饰符会被隐形继承的。 private 也被集成,只事派生类没有访问权限而已 virtual可加可不加 子类的空间里有父类的所有变量(static除外) 同一个函数只存在一个实体(inline除外) 子类覆盖它的函数不加virtual ,也能实现多态。 在子类的空间里,有父类的私有变量。私有变量不能直接访问。 2.输入一个字符串,将其逆序后输出。(使用C++,不建议用伪码) #include using namespace std; void main() { char a[50];memset(a,0,sizeof(a)); int i=0,j; char t; cin.getline(a,50,'\n'); for(i=0,j=strlen(a)-1;i++{ t=a[i]; a[i]=a[j]; a[j]=t; } cout<<a<<endl; } 36.下面是C语言中两种if语句判断方式。请问哪种写法更好?为什么? int n; if (n == 10) // 第一种判断方式 if (10 == n) // 第二种判断方式 如果少了个=号,编译时就会报错,减少了出错的可能行,可以检测出是否少了= 10.下面代码有什么问题? Void test2() { char string[10], str1[10]; for(i=0; i<10;i++) { str1[i] ="a"; } strcpy(string, str1); } 数组越界,strcpy拷贝的结束标志是查找字符串中的\0 因此如果字符串中没有遇到\0的话 会一直复制,直到遇到\0,上面的123都因此产生越界的情况 建议使用 strncpy 和 memcpy 15.用C++写个程序,如何判断一个操作系统是16位还是32位的?不能用sizeof()函数 A1: 16位的系统下, int i = 65536; cout << i; // 输出0; int i = 65535; cout << i; // 输出-1; 32位的系统下, int i = 65536; cout << i; // 输出65536; int i = 65535; cout << i; // 输出65535; A2: int a = ~0; if( a>65536 ) { cout<<"32 bit"<} else { cout<<"16 bit"<} 17.在不用第三方参数的情况下,交换两个参数的值 #include void main() { int i=60; int j=50; i=i+j; j=i-j; i=i-j; printf("i=%d\n",i); printf("j=%d\n",j); } 方法二: i^=j; j^=i; i^=j; 方法三: // 用加减实现,而且不会溢出 a = a+b-(b=a) 20.进程间通信的方式有? 进程间通信的方式有 共享内存, 管道 ,Socket ,消息队列 , DDE等 22.下面的函数实现在一个固定的数上加上一个数,有什么错误,改正 int add_n(int n) { static int i=100; i+=n; return i; } 答: 因为static使得i的值会保留上次的值。 去掉static就可了 23.下面的代码有什么问题? class A { public: A() { p=this; } ~A() { if(p!=NULL) { delete p; p=NULL; } } A* p; }; 答: 会引起无限递归 虚函数的定义要遵循以下重要规则: 1.如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,那么即使加上了virtual关键字,也是不会进行滞后联编的。 2.只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。 3.静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。 4.内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义定义,但是在编译的时候系统仍然将它看做是非内联的。 5.构造函数不能是虚函数,因为构造的时候,对象还是一片位定型的空间,只有构造完成后,对象才是具体类的实例。 6.析构函数可以是虚函数,而且通常声名为虚函数。 在编译时就能够确定哪个重载的成员函数被调用的情况被称做先期联编(early binding),而在系统能够在运行时,能够根据其类型确定调用哪个重载的成员函数的能力,称为多态性,或叫滞后联编(late binding) /例程3 #include <iostream> using namespace std; class Vehicle { public: Vehicle(float speed,int total) { Vehicle::speed = speed; Vehicle::total = total; } virtual void ShowMember()//虚函数 { cout<<speed<<"|"<<total<<endl; } protected: float speed; int total; }; class Car:public Vehicle { public: Car(int aird,float speed,int total):Vehicle(speed,total) { Car::aird = aird; } virtual void ShowMember()//虚函数,在派生类中,由于继承的关系,这里的virtual也可以不加 { cout<<speed<<"|"<<total<<"|"<<aird<<endl; } public: int aird; }; void test(Vehicle &temp) { temp.ShowMember(); } int main() { Vehicle a(120,4); Car b(180,110,4); test(a); test(b); cin.get(); } 运行结果:120|4 110|4|180 //例程2 #include <iostream> using namespace std; class Vehicle { public: Vehicle(float speed,int total) { Vehicle::speed=speed; Vehicle::total=total; } void ShowMember() { cout<<speed<<"|"<<total<<endl; } protected: float speed; int total; }; class Car:public Vehicle { public: Car(int aird,float speed,int total):Vehicle(speed,total) { Car::aird=aird; } void ShowMember() 没有用virtual,则在子类调用该函数时调用的是父类中的ShowMenmber函数。虚函数就是覆盖,即基类中的虚函数被派生类中的同名函数所覆盖。 { cout<<speed<<"|"<<total<<"|"<<aird<<endl; } protected: int aird; }; void test(Vehicle &temp) { temp.ShowMember(); } void main() { Vehicle a(120,4); Car b(180,110,4); test(a); test(b); cin.get(); } 运行结果:120|4 110|4 2、 析构函数是虚函数的优点是什么 用C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明: 有下面的两个类: class ClxBase { public: ClxBase() {}; virtual ~ClxBase() {}; virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; }; }; class ClxDerived : public ClxBase { public: ClxDerived() {}; ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; }; void DoSomething() { cout << "Do something in class ClxDerived!" << endl; }; }; ClxBase *pTest = new ClxDerived; pTest->DoSomething(); delete pTest; 输出结果是: Do something in class ClxDerived! Output from the destructor of class ClxDerived! 这个很简单,非常好理解。 但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了: Do something in class ClxDerived! 也就是说,类ClxDerived的析构函数根本没有被调用!一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。 所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。 当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。 38.用C 写一个输入的整数,倒着输出整数的函数,要求用递归方法 ; 答: void fun( int a ) { printf( "%d", a%10 ); a /= 10; if( a <=0 )return; fun( a ); } ①链表反转 单向链表的反转是一个经常被问到的一个面试题,也是一个非常基础的问题。比如一个链表是这样的: 1->2->3->4->5 通过反转后成为5->4->3->2->1。 最容易想到的方法遍历一遍链表,利用一个辅助指针,存储遍历过程中当前指针指向的下一个元素,然 后将当前节点元素的指针反转后,利用已经存储的指针往后面继续遍历。源代码如下: struct linka { int data; linka* next; }; void reverse(linka*& head) { if(head ==NULL) return; linka *pre, *cur, *ne; pre=head; cur=head->next; while(cur) { ne = cur->next; cur->next = pre; pre = cur; cur = ne; } head->next = NULL; head = pre; } 还有一种利用递归的方法。这种方法的基本思想是在反转当前节点之前先调用递归函数反转后续节点。 源代码如下。不过这个方法有一个缺点,就是在反转后的最后一个结点会形成一个环,所以必须将函数的 返回的节点的next域置为NULL。因为要改变head指针,所以我用了引用。算法的源代码如下: linka* reverse(linka* p,linka*& head) { if(p == NULL || p->next == NULL) { head=p; return p; } else { linka* tmp = reverse(p->next,head); tmp->next = p; return p; } } 接下来我们按照编译顺序看看编译器每一步都做了什么: cpp hw.c -o hw.i // 预处理 gcc -E hello.c -o hello.i cc1 hw.i -o hw.s // 编译 gcc -S hello.i -o hello.s as hw.s -o hw.o // 汇编 gcc -c hello.s -o hello.o ld hw.o -o hw.exe // 链接 gcc hello.o -o hello.exe C++函数中那些不可以被声明为虚函数 常见的不不能声明为虚函数的有:普通函数(非成员函数);静态成员函数;内联成员函数;构造函数;友元函数。 1.为什么C++不支持普通函数为虚函数? 普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时邦定函数。 2.为什么C++不支持构造函数为虚函数? 这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作。(这不就是典型的悖论) 3.为什么C++不支持内联成员函数为虚函数? 其实很简单,那内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。(再说了,inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数) 4.为什么C++不支持静态成员函数为虚函数? 这也很简单,静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态邦定的必要性。 5.为什么C++不支持友元函数为虚函数? 因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。 请你分别划划OSI的七层网络结构图,和TCP/IP的五层结构图? 请你详细的解释一下IP协议的定义,在哪个层上面,主要有什么作用? TCP与UDP呢? 请问交换机和路由器分别的实现原理是什么?分别在哪个层次上面实现的?转载地址:http://fvcrn.baihongyu.com/