文件操作:iostream。读取数据ifstream,写入数据ofstream。 数据存储:vector 排序算法:冒泡排序 |
#include
#include
using namespace std;
void Order(vector
{
int count = data.size() ;
for ( int i = 0 ; i < count ; i++)
{
for ( int j = 0 ; j < count - i - 1 ; j++)
{
if ( data[j] > data[j+1])
{
int temp = data[j] ;
data[j] = data[j+1] ;
data[j+1] = temp ;
}
}
}
void main( void )
{
vector
ifstream in("c:\data.txt");
if ( !in)
{
cout<<"file error!";
exit(1);
}
int temp;
//读入文件数据
while (!in.eof())
{
in>>temp;
data.push_back(temp);
}
in.close(); //关闭输入文件流
Order(data);
ofstream out("c:\result.txt");
if ( !out)
{
cout<<"file error!";
exit(1);
}
for ( i = 0 ; i < data.size() ; i++)
out<
out.close(); //关闭输出文件流
}
考察对链表模型的理解和指针操作。 定义结构体使用 typedef关键字进行重命名。 |
typedef struct Node
{
int data ;
Node *next ;
} Node;
(1)已知链表的头结点head,写一个函数把这个链表逆序 (Intel)
思路: 记录指向head的指针pHead,遍历链表直到末尾( p.next != NULL),执行以下操作: 记录节点i(i从head开始)的指针p1 记录节点i+1的指针p2 记录节点i+2的指针p3 变更指针指向: p2.next = p1; p1= p2; p2= p3; p3= p3.next; 循环中执行当前操作,直到p3.next == NULL 设置pHead = p3; |
Node * ReverseList(Node *head) //链表逆序
{
if ( head == NULL || head->next == NULL )
return head;
Node *p1 = head ;
Node *p2 = p1->next ;
Node *p3 = p2->next ;
p1->next = NULL ;
while ( p3 != NULL )
{
p2->next = p1 ;
p1 = p2 ;
p2 = p3 ;
p3 = p3->next ;
}
p2->next = p1 ;
head = p2 ;
return head ;
}
(2)已知两个链表head1 和head2 各自有序,请把它们合并成一个链表依然有序。(保留所有结点,即便大小相同)
Node * Merge(Node *head1 , Node *head2)
{
if ( head1 == NULL)
return head2 ;
if ( head2 == NULL)
return head1 ;
Node *head = NULL ;
Node *p1 = NULL;
Node *p2 = NULL;
//1.默认有序链表排列顺序是升序
//选择头结点
if ( head1->data < head2->data )
{
head = head1 ;
p1 = head1->next;
p2 = head2 ;
}
else
{
head = head2 ;
p2 = head2->next ;
p1 = head1 ;
}
//2.对比两个链表中元素大小,将小值连接到合并链表末尾,然后移动指针后移
//记录和并链表当前元素指针pCurrent
Node *pcurrent = head ;
while ( p1 != NULL && p2 != NULL)
{
if ( p1->data <= p2->data )
{
pcurrent->next = p1 ;
pcurrent = p1 ;
p1 = p1->next ;
}
else
{
pcurrent->next = p2 ;
pcurrent = p2 ;
p2 = p2->next ;
}
}
//3.将未处理完的链表数据全部加入合并列表
if ( p1 != NULL )
pcurrent->next = p1 ;
if ( p2 != NULL )
pcurrent->next = p2 ;
//返回合并后链表头指针
return head ;
}
(3)已知两个链表head1 和head2 各自有序,请把它们合并成一个链表依然有序,这次要求用递归方法进行。 (Autodesk)(待验证)
答案:
Node * MergeRecursive(Node *head1 , Node *head2)
{
if ( head1 == NULL )
return head2 ;
if ( head2 == NULL)
return head1 ;
Node *head = NULL ;
if ( head1->data < head2->data )
{
head = head1 ;
head->next = MergeRecursive(head1->next,head2);
}
else
{
head = head2 ;
head->next = MergeRecursive(head1,head2->next);
}
return head ;
----------
41. 分析一下这段程序的输出 (Autodesk)(待验证)
class B
{
public:
B()
{
cout<<"default constructor"<
}
~B()
{
cout<<"destructed"<
}
B(int i):data(i) //B(int) works as a converter ( int -> instance of B)
{
cout<<"constructed by parameter " << data <
}
private:
int data;
};
B Play( B b)
{
return b ;
}
(1)
int main(int argc, char* argv[])
{
B t1 = Play(5); B t2 = Play(t1);
return 0;
}
(2)
int main(int argc, char* argv[])
{
B t1 = Play(5); B t2 = Play(10);
return 0;
}
代码段1:
int main(int argc, char* argv[])
{
cout << "t1" << endl;
B t1 = Play(5); // constructed by parameter 5
// destructed Play临时创建的B的对象副本
cout << "t2" << endl;
B t2 = Play(t1);
// destructed Play临时创建的t1的副本
cout << "end" << endl;
return 0;
}
// destructed t2
// destructed t1
代码段2:
int main(int argc, char* argv[])
{
cout << "t1" << endl;
B t1 = Play(5); // constructed by parameter 5
// destructed Play临时创建的B的对象副本
cout << "t2" << endl;
B t2 = Play(10); // constructed by parameter 10
// destructed Play临时创建的B的对象副本
cout << "end" << endl;
return 0;
}
// destructed t2
// destructed t1
思路:与遍历数组查找最大数相同,不过此处需记录第二的数,且在最大数更新时将被替换掉的次大数记录在第二大的数变量中。 |
答案:
const int MINNUMBER = -32767 ;
int find_sec_max( int data[] , int count)
{
int maxnumber = data[0] ;
int sec_max = MINNUMBER ;
for ( int i = 1 ; i < count ; i++)
{
if ( data[i] > maxnumber )
{
sec_max = maxnumber ;
maxnumber = data[i] ;
}
else
{
if ( data[i] > sec_max )
sec_max = data[i] ;
}
}
return sec_max ;
}
KMP算法效率最好,时间复杂度是O(n+m),
46.多重继承的内存分配问题*:
比如有class A : public class B, public class C {} 那么A的内存结构大致是怎么样的?
这个是compiler-dependent的, 不同的实现其细节可能不同。如果不考虑有虚函数、虚继承的话就相当简单;否则的话,相当复杂。可以参考《深入探索C++对象模型》。
链接:多重继承和虚继承的内存布局
47.如何判断一个单链表是有环的?(注意不能用标志位,最多只能用两个额外指针)
struct node { char val; node* next;}
bool check(const node* head) {} //return false : 无环;true: 有环一种O(n)的办法就是(搞两个指针,一个每次递增一步,一个每次递增两步,如果有环的话两者必然重合,反之亦然):
bool check(const node* head)
{
if(head==NULL) return false;
node *low=head, *fast=head->next;
while(fast!=NULL && fast->next!=NULL)
{
low=low->next;
fast=fast->next->next;
if(low==fast) return true;
}
return false;
}
分析这些面试题,本身包含很强的趣味性;而作为一名研发人员,通过对这些面试题的深入剖析则可进一步增强自身的内功。
2.找错题 试题1:
以下是引用片段:
void test1() //数组越界
{
char string[10];
char* str1 = "0123456789";
strcpy( string, str1 );
}
试题2:
以下是引用片段:
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1= 'a';
}
strcpy( string, str1 );
}
试题3:
以下是引用片段:
void test3(char* str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}
解答:
试题1:字符串str1需要11个字节才能存放下(包括末尾的’\0’),而string只有10个字节的空间,strcpy会导致数组越界;
试题2:如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string,str1)调用使得从 str1内存起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分;
试题3:if(strlen(str1) <= 10)应改为if(strlen(str1) <10),因为strlen的结果未统计’\0’所占用的1个字节。剖析:考查对基本功的掌握
(1)字符串以’\0’结尾;
(2)对数组越界把握的敏感度;
(3)库函数strcpy的工作方式。
C字符串:字符串末尾会有一个结束标记'\0'标识字符串结束,需要占用1个字节位置,(试题1)str1最多只能存放9个字符的字符串,或者10个字符(没有'\0'不能成为字符串,只是字符数组)。 strcpy()工作方式:以源串中的'/0'为拷贝结束标志,直到遇到该'\0'为止,然后将'\0'拷贝上.
strncpy()以第三个参数N为拷贝结束标志,如果source的长度小于N,则剩余的字符全部用'\0'填充; 如果source的长度大于N,则从source中截取前N个字符,拷贝过去.不包括'\0'
详细教程
strcpy与strncpy工作方式及其区别
|
试题4:以下是引用片段:
void GetMemory( char *p )
{
p = (char *) malloc( 100 );
}
void Test( void )
{
char *str = NULL;
GetMemory( str );
strcpy( str, "hello world" );
printf( str );
}
试题5:以下是引用片段:
char *GetMemory( void )
{
char p[] = "hello world"; //p是函数内部的局部变量,函数返回后指向字符串的指针已释放
return p;
}
void Test( void )
{
char *str = NULL;
str = GetMemory();
printf( str ); //打印乱码
}
试题6:以下是引用片段:
void GetMemory( char **p, int num )
{
*p = (char *) malloc( num );
}
void Test( void )
{
char *str = NULL;
GetMemory( &str, 100 );
strcpy( str, "hello" );
printf( str ); //未进行内存释放
}
试题7:以下是引用片段:
void Test( void )
{
char *str = (char *) malloc( 100 );
strcpy( str, "hello" );
free( str );
... //省略的其它语句
}
解答:
试题4:传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完
char *str = NULL;
GetMemory( str );
后的str仍然为NULL;
试题5:
char p[] = "hello world";
return p;
的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。
试题6:GetMemory函数避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句
*p = (char *) malloc( num );
后未判断内存是否申请成功(未考虑内存申请失败处理),应加上:
if ( *p == NULL )
{
...//进行申请内存失败处理
}
试题7:存在与试题6同样的问题,在执行
char *str = (char *) malloc(100);
后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个“野”指针,应加上:
str = NULL;
试题6的Test函数中也未对malloc的内存进行释放。
剖析:试题4~7考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60的错误。但是要完全解答正确,却也绝非易事。
对内存操作的考查主要集中在:
(1)指针的理解;
(2)变量的生存期及作用范围;
(3)良好的动态内存申请和释放习惯。
再看看下面的一段程序有什么错误:
以下是引用片段:
swap( int* p1,int* p2 )
{
int *p;
*p = *p1; //p未分配存放数据的空间,不能直接赋值
*p1 = *p2;
*p2 = *p;
}
在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG运行时提示错误“Access Violation”。该程序应该改为
以下是引用片段:
swap( int* p1,int* p2 )
{
int p;
p = *p1;
*p1 = *p2;
*p2 = p;
}
总分值为10,下面给出几个不同得分的答案:2分 以下是引用片段:
void strcpy( char *strDest, char *strSrc )
{
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
4分 以下是引用片段:
void strcpy( char *strDest, const char *strSrc )
//将源字符串加const,表明其为输入参数,加2分
{
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
7分 以下是引用片段:
void strcpy(char *strDest, const char *strSrc)
{
//对源地址和目的地址加非0断言,加3分
assert( (strDest != NULL) &&(strSrc != NULL) );
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
10分 以下是引用片段:
//为了实现链式操作,将目的地址返回,加3分!
char * strcpy( char *strDest, const char *strSrc ) //对不做修改的参数使用const关键字
{
assert( (strDest != NULL) &&(strSrc != NULL) ); //传递参数有效性判断(不为NULL)
char *address = strDest; //记录传入参数指针首地址(返回值字符串指针首地址)
while( (*strDest++ = * strSrc++) != ‘\0’ ); //实现内容拷贝
return address;
}
从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!需要多么扎实的基本功才能写一个完美的strcpy啊!
(4)对strlen的掌握,计算字符串长度时不包括字符串末尾的'\0'(但以'\0'作为查找结束标识)。
读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为: int strlen( const char *str ) //输入参数const 以下是引用片段:
{
assert( strt != NULL ); //断言字符串地址非0
int len=0; //注,一定要初始化。
while( (*str++) != '\0' )
{
len++;
}
return len;
}
已知String类定义如下:
class String
{
public:
String(const char *str = NULL); // 通用构造函数
String(const String &another); // 拷贝构造函数
~String(); // 析构函数
String& operater =(const String &rhs); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
尝试写出类的成员函数实现。
答案:
String::String(const char *str)
{
if ( str == NULL ) //strlen在参数为NULL时会抛异常才会有这步判断
{
m_data = new char[1] ;
m_data[0] = '\0' ;
}
else
{
m_data = new char[strlen(str) + 1];
strcpy(m_data,str);
}
}
String::String(const String &another)
{
m_data = new char[strlen(another.m_data) + 1];
strcpy(m_data,other.m_data);
}
String& String::operator =(const String &rhs)
{
if ( this == &rhs)
return *this ;
delete[] m_data; //删除原来的数据,新开一块内存
m_data = new char[strlen(rhs.m_data) + 1];
strcpy(m_data,rhs.m_data);
return *this ;
}
String::~String()
{
delete[] m_data ;
}
答:防止该头文件被重复引用。
答:前者是从Standard Library的路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h。
C++语言支持函数重载,C语言不支持函数重载。C++提供了C连接交换指定符号extern “C”解决名字匹配问题。
首先,作为extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数
extern "C"是连接申明(linkage declaration),被extern "C"修饰的变量和函数是按照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++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
(1) 未加extern "C"声明时的连接方式
假设在C++中,模块A的头文件如下:
// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
在模块B中引用该函数:
// 模块B实现文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);
(2) 加extern "C"声明后的编译和连接方式
加extern "C"声明后,模块A的头文件变为:
// 模块A头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif
在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:
(1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;
(2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。
如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。
所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):实现C++与C及其它语言的混合编程。
明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧:
extern "C"的惯用法
(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
extern "C"
{
#include "cExample.h"
}
而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。
C++引用C函数例子工程中包含的三个文件的源代码如下:
/* c语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++实现文件,调用add:cppFile.cpp
extern "C"
{
#include "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" {}。
(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
C引用C++函数例子工程中包含的三个文件的源代码如下:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
15题目的解答请参考《C++中extern “C”含义深层探索》注解:
几道c笔试题(含参考答案)
1. What is displayed when f() is called given the code:
class Number {
1.完成下列程序
*
*.*.
*..*..*..
*...*...*...*...
*....*....*....*....*....
*.....*.....*.....*.....*.....*.....
*......*......*......*......*......*......*......
*.......*.......*.......*.......*.......*.......*.......*.......
#include
#define N 8
int main()
{
int i;
int j;
int k;
---------------------------------------------------------
| |
| |
| |
---------------------------------------------------------
return 0;
}
#include
using namespace std;
#define N 8
int main()
{
int i;
int j;
int k;
//line
for(int i = 0; i < N; i++)
{
//*
for(int j = i; j >= 0; j--)
{
cout << "*";
//.
for(int k = i - 1; k >= 0; k--)
{
cout << ".";
}
}
cout << endl;
}
return 0;
}
|
应用层:为应用程序提供服务
表示层:处理在两个通信系统中交换信息的表示方式
会话层:负责维护两个结点间会话连接的建立、管理和终止,以及数据交换
传输层:向用户提供可靠的端到端服务。UDP TCP协议。
网络层:通过路由选择算法为分组通过通信子网选择最适当的路径,以及实现拥塞控制、网络互联等功能。数据传输单元是分组。IP地址,路由器,IP协议。
数据链路层:在物理层提供的服务基础上,数据链路层在通信的实体间建立数据链路连接,传输一帧为单位的数据包(,并采用差错控制与流量控制方法,使有差错的物理线路变成无差错的数据链路。)
物理层:传输比特流。传输单元是比特。调制解调器。
(1) OSI七层模型
OSI中的层 功能 TCP/IP协议族 应用层 文件传输,电子邮件,文件服务,虚拟终端 TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 表示层 数据格式化,代码转换,数据加密 没有协议 会话层 解除或建立与别的接点的联系 没有协议 传输层 提供端对端的接口 TCP,UDP 网络层 为数据包选择路由 IP,ICMP,RIP,OSPF,BGP,IGMP 数据链路层 传输有地址的帧以及错误检测功能 SLIP,CSLIP,PPP,ARP,RARP,MTU 物理层 以二进制数据形式在物理媒体上传输数据 ISO2110,IEEE802,IEEE802.2 (2)TCP/IP 五层模型的协议
应用层 传输层 网络层 数据链路层 物理层
链接: http://www.2cto.com/net/201310/249035.html
|
网络层。
交换机:数据链路层。路由器:网络层。
全局变量的生命周期是整个程序运行的时间,而局部变量的生命周期则是局部函数或过程调用的时间段。其实现是由编译器在编译时采用不同内存分配方法。全局变量在main函数调用后,就开始分配,如果是静态变量则是在main函数前就已经初始化了。而局部变量则是在用户栈中动态分配的(还是建议看编译原理中的活动记录这一块)
8086微处理器共有4个16位的段寄存器,在寻址内存单元时,用它们直接或间接地存放段地址。
代码段寄存器CS:存放当前执行的程序的段地址。
数据段寄存器DS:存放当前执行的程序所用操作数的段地址。
堆栈段寄存器SS:存放当前执行的程序所用堆栈的段地址。
附加段寄存器ES:存放当前执行程序中一个辅助数据段的段地址。
由cs:ip构成指令地址,ss:sp构成堆栈的栈顶地址指针。DS和ES用作数据段和附加段的段地址(段起始地址或段值)
8086/8088微处理器的存储器管理
1.地址线(码)与寻址范围:N条地址线 寻址范围=2N
2.8086有20地址线 寻址范围为1MB 由 00000H~FFFFFH
3. 8086微处理器是一个16位结构,用户可用的寄存器均为16位:寻址64KB
4. 8086/8088采用分段的方法对存储器进行管理。具体做法是:把1MB的存储器空间分成若干段,每段容量为64KB,每段存储器的起始地址必须是一个能被16整除的地址码,即在20位的二进制地址码中最低4位必须是“0”。每个段首地址的高16位二进制代码就是该段的段号(称段基地址)或简称段地址,段号保存在段寄存器中。我们可对段寄存器设置不同的值来使微处理器的存储器访问指向不同的段。
5.段内的某个存储单元相对于该段段首地址的差值,称为段内偏移地址(也叫偏移量)用16位二进制代码表示。
6.物理地址是由8086/8088芯片地址引线送出的20位地址码,它用来参加存储器的地址译码,最终读/写所访问的一个特定的存储单元。
7.逻辑地址由某段的段地址和段内偏移地址(也叫偏移量)两部分所组成。写成:
段地址:偏移地址(例如,1234H:0088H)。
8.在硬件上起作用的是物理地址,物理地址=段基地址×10H十偏移地址
联想笔试题
1.设计函数 int atoi(char *s)。
2.int i=(j=4,k=8,l=16,m=32); printf(“%d”, i); 输出是多少?