您的位置:首页 > 编程语言 > C语言/C++

C/C++笔试面试常见问题1

2013-10-31 09:42 411 查看
第一 、 STL中智能指针的缺陷

因为auto_ptr并不是完美无缺的,它的确很方便,但也有缺陷,在使用时要注意避免。首先,不要将auto_ptr对象作为STL容器的元素。C++标准明确禁止这样做,否则可能会碰到不可预见的结果

auto_ptr的另一个缺陷是将数组作为auto_ptr的参数: auto_ptr<char> pstr (new char[12] ); //数组;为定义

然后收集了关于auto_ptr的几种注意事项:

1、auto_ptr不能共享所有权。

2、auto_ptr不能指向数组

3、auto_ptr不能作为容器的成员。

4、不能通过赋值操作来初始化auto_ptr

std::auto_ptr<int> p(new int(42)); //OK

std::auto_ptr<int> p = new int(42); //ERROR

这是因为auto_ptr 的构造函数被定义为了explicit

5、不要把auto_ptr放入容器

第二、宏与内联函数的区别

#define IQUEUE_IS_EMPTY(entry) ((entry) == (entry)->next)

#define iqueue_is_empty IQUEUE_IS_EMPTY

...

if (iqueue_is_empty(&kcp->rcv_queue))

return -1;

(1)什么是内联函数?

内联函数是指那些定义在类体内的成员函数,即该函数的函数体放在类体内。

(2)为什么要引入内联函数?

当然,引入内联函数的主要目的是:解决程序中函数调用的效率问题。但是宏的定义很容易产生二意性。

(3)为什么inline能取代宏?

inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。

很明显,类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。

inline 可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。

(4)内联函数和宏的区别?

内联函数和宏的区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。你可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。内联函数与带参数的宏定义进行下比较,它们的代码效率是一样,但是内联欢函数要优于宏定义,因为内联函数遵循的类型和作用域规则,它与一般函数更相近,在一些编译器中,一旦关上内联扩展,将与一般函数一样进行调用,比较方便。

(5)什么时候用内联函数?

内联函数在C++类中,应用最广的,应该是用来定义存取函数。我们定义的类中一般会把数据成员定义成私有的或者保护的,这样,外界就不能直接读写我们类成员的数据了。对于私有或者保护成员的读写就必须使用成员接口函数来进行。如果我们把这些读写成

员函数定义成内联函数的话,将会获得比较好的效率。

Class A

{

Private:

int nTest;

 Public:

int readtest() { return nTest;}

void settest(int I) { nTest=I; }

}

(6)如何使用内联函数?

我们可以用inline来定义内联函数。

inline int A (int x) { return 2*x; }

不过,任何在类的说明部分定义的函数都会被自动的认为是内联函数。

(7)内联函数的优缺点?

我们可以把它作为一般的函数一样调用,但是由于内联函数在需要的时候,会像宏一样展开,所以执行速度确比一般函数的执行速度要快。当然,内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。(换句话说就是,你使用内联函数,只不过是向编译器提出一个申请,编译器可以拒绝你的申请)这样,内联函数就和普通函数执行效率一样了。

(8)如何禁止函数进行内联?

如果使用VC++,可以使用/Ob命令行参数。当然,也可以在程序中使用 #pragma auto_inline达到相同的目的。

(9)注意事项:

1.在内联函数内不允许用循环语句和开关语句。

2.内联函数的定义必须出现在内联函数第一次被调用之前。

第三、指针与引用的区别

指针能够毫无约束地操作内存中的如何东西,功能强大,但是使用不当非常危险。

引用仅是借用对象的别名,使用方便,不会意外的操作其他内存空间。

第四、STL 关于迭代器失效问题

当一个容器变化时,指向该容器中元素的迭代器可能失效。这使得在迭代器变化期间改变容器容易出现问题。在这方面,不同的容器提供不同的保障:

vectors: 引起内存重新分配的插入运算使所有迭代器失效,插入也使得插入位置及其后位置的迭代器失效,删除运算使得删除位置及其后位置的迭代器失效.

vector的push_back操作可能没事,但是一旦引发内存重分配,所有迭代器都会失效;

vector的insert操作插入点之后的所有迭代器失效;但一旦引发内存重分配,所有迭代器都会失效;

vector的erase操作插入点之后的所有迭代器失效;

vector的reserve操作所有迭代器失效(因为它导致内存重分配);

list/map: 插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.

deque的insert操作所有迭代器失效;

deque的erase操作所有迭代器失效;

1. 对于关联容器(如map, set, multimap,multiset),删除当前的iterator,仅仅会使当前的iterator失效,只要在erase时,递增当前iterator即可。这是因为map之类的容器,使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响。erase只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器

for (iter = cont.begin(); it != cont.end();)

{

(*iter)->doSomething();

if (shouldDelete(*iter))

cont.erase(iter++);

else

++iter;

}

2. 对于序列式容器(如vector,deque),删除当前的iterator会使后面所有元素的iterator都失效。这是因为vetor,deque使用了连续分配的内存,删除一个元素导致后面所有的元素会向前移动一个位置。所以不能使用erase(iter++)的方式,还好erase方法可以返回下一个有效的iterator。

for (iter = cont.begin(); iter != cont.end();)

{

(*it)->doSomething();

if (shouldDelete(*iter))

iter = cont.erase(iter);

else

++iter;

}

3. 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种方法都可以使用。

第五、linux中以及有几种信号以及如何加载core文件gdb

当我们的程序崩溃时,内核有可能把该程序当前内存映射到core文件里,方便程序员找到程序出现问题的地方。最常出现的,几乎所有C程序员都出现过的错误就是“段错误”了。也是最难查出问题原因的一个错误。下面我们就针对“段错误”来分析core文件的产生、以及我们如何利用core文件找到出现崩溃的地方。

何谓core文件:当一个程序崩溃时,在进程当前工作目录的core文件中复制了该进程的存储图像。core文件仅仅是一个内存映象(同时加上调试信息),主要是用来调试的。

当程序接收到以下UNIX信号会产生core文件:

在系统默认动作列,“终止w/core”表示在进程当前工作目录的core文件中复制了该进程的存储图像(该文件名为core,由此可以看出这种功能很久之前就是UNIX功能的一部分)。大多数UNIX调试程序都使用core文件以检查进程在终止时的状态。

core文件的产生不是POSIX.1所属部分,而是很多UNIX版本的实现特征。UNIX第6版没有检查条件(a)和(b),并且其源代码中包含如下说明:“如果你正在找寻保护信号,那么当设置-用户-ID命令执行时,将可能产生大量的这种信号”。4.3 + BSD产生名为core.prog的文件,其中prog是被执行的程序名的前1 6个字符。它对core文件给予了某种标识,所以是一种改进特征。

表中“硬件故障”对应于实现定义的硬件故障。这些名字中有很多取自UNIX早先在DP-11上的实现。请查看你所使用的系统的手册,以确切地确定这些信号对应于哪些错误类型。

下面比较详细地说明这些信号。



SIGABRT 调用abort函数时产生此信号。进程异常终止。

SIGBUS 指示一个实现定义的硬件故障。

SIGEMT 指示一个实现定义的硬件故障。

EMT这一名字来自PDP-11的emulator trap 指令。

SIGFPE 此信号表示一个算术运算异常,例如除以0,浮点溢出等。

SIGILL 此信号指示进程已执行一条非法硬件指令。

4.3BSD由abort函数产生此信号。SIGABRT现在被用于此。

SIGIOT 这指示一个实现定义的硬件故障。

IOT这个名字来自于PDP-11对于输入/输出TRAP(input/output TRAP)指令的缩写。系统V的早期版本,由abort函数产生此信号。SIGABRT现在被用于此。

SIGQUIT 当用户在终端上按退出键(一般采用Ctrl-/)时,产生此信号,并送至前台进

程组中的所有进程。此信号不仅终止前台进程组(如SIGINT所做的那样),同时产生一个core文件。

SIGSEGV 指示进程进行了一次无效的存储访问。

名字SEGV表示“段违例(segmentation violation)”。

SIGSYS 指示一个无效的系统调用。由于某种未知原因,进程执行了一条系统调用指令,

但其指示系统调用类型的参数却是无效的。

SIGTRAP 指示一个实现定义的硬件故障。

此信号名来自于PDP-11的TRAP指令。

SIGXCPU SVR4和4.3+BSD支持资源限制的概念。如果进程超过了其软C P U时间限制,则产生此信号。

SIGXFSZ 如果进程超过了其软文件长度限制,则SVR4和4.3+BSD产生此信号。

摘自《UNIX环境高级编程》第10章 信号。

例子:

#include <stdio.h>
 #include <string.h>

 int func(char **a)
 {
     a[0]="abc"; //有多少次赋值是不确定的,就是说字符串个数不定
     a[1]="def";
     a[2]="ghi";
 }
 int main()
 {
     char** array;
    printf("array=%d\n",array);
    func(array);
    printf("array[0]=%s\n",array[0]);
     printf("array[1]=%s\n",array[1]);
 }

编译:

[zhanghua@localhost core_dump]$ gcc –g core_dump_test.c -o core_dump_test

如果需要调试程序的话,使用gcc编译时加上-g选项,这样调试core文件的时候比较容易找到错误的地方。

执行:

[zhanghua@localhost core_dump]$ ./core_dump_test

段错误

运行core_dump_test程序出现了“段错误”,但没有产生core文件。这是因为系统默认core文件的大小为0,所以没有创建。可以用ulimit命令查看和修改core文件的大小。

[zhanghua@localhost core_dump]$ ulimit -c

0

[zhanghua@localhost core_dump]$ ulimit -c 1000

[zhanghua@localhost core_dump]$ ulimit -c

1000

-c 指定修改core文件的大小,1000指定了core文件大小。也可以对core文件的大小不做限制,如:

[zhanghua@localhost daemon]# ulimit -c unlimited

[zhanghua@localhost daemon]# ulimit -c

unlimited

如果想让修改永久生效,则需要修改配置文件,如.bash_profile、/etc/profile或/etc/security/limits.conf。

再次执行:

[zhanghua@localhost core_dump]$ ./core_dump_test

段错误 (core dumped)

[zhanghua@localhost core_dump]$ ls core.*

core.6133

可以看到已经创建了一个core.6133的文件.6133是core_dump_test程序运行的进程ID。

调式core文件

core文件是个二进制文件,需要用相应的工具来分析程序崩溃时的内存映像。



GDB中键入where,就会看到程序崩溃时堆栈信息(当前函数之前的所有已调用函数的列表(包括当前函数),gdb只显示最近几个),我们很容易找到我们的程序在最后崩溃的时候调用了core_dump_test.c 第7行的代码,导致程序崩溃。注意:在编译程序的时候要加入选项-g。您也可以试试其他命令, 如 fram、list等。更详细的用法,请查阅GDB文档。

core文件创建在什么位置

在进程当前工作目录的下创建。通常与程序在相同的路径下。但如果程序中调用了chdir函数,则有可能改变了当前工作目录。这时core文件创建在chdir指定的路径下。有好多程序崩溃了,我们却找不到core文件放在什么位置。和chdir函数就有关系。当然程序崩溃了不一定都产生core文件。

什么时候不产生core文件

在下列条件下不产生core文件:

( a )进程是设置-用户-ID,而且当前用户并非程序文件的所有者;

( b )进程是设置-组-ID,而且当前用户并非该程序文件的组所有者;

( c )用户没有写当前工作目录的许可权;

( d )文件太大。core文件的许可权(假定该文件在此之前并不存在)通常是用户读/写,组读和其他读。

利用GDB调试core文件,当遇到程序崩溃时我们不再束手无策。

第六、C++初始化列表以及顺序

在C++中,构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。

这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。

构造函数初始化表的使用规则:

1.如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。

例如

class A

{…

A(int x); // A 的构造函数

};

class B : public A

{…

B(int x, int y);// B 的构造函数

};

B::B(int x, int y): A(x) // 在初始化表里调用A 的构造函数

{



}

2.类的const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式

来初始化

class Shape

{

const int m_size; //const 常量

float m_width;

float m_height;

public:

Shape(int s,float w,float h):m_size(s) //只能在这初始化

{

//m_size =s; //在初始化将出错

m_width = w;

m_height = h;

}



};

3. 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。

方式一:在初始化列表中初始化

class Line

{

.........

};

class Shape

{

float m_width;

float m_height;

Line m_line;

public:

Shape(float w,float h,Line line):m_line(line)

{

m_width = w;

m_height = h;



}



};

方式二:在构造函数内部初始化

class Line

{

.........

};

class Shape

{

float m_width;

float m_height;

Line m_line;

public:

Shape(float w,float h,Line line)

{

m_line = line;

m_width = w;

m_height = h;



}



};

二者区别在与:前者效率高于后者,因为前者只调用拷贝构造函数,而后者调用了构造和拷贝构造函数.

简单总结三条:(使用初始化列表)

a.类存在继承关系;

b.类的const常量;

c.类的数据成员的初始化(非内部数据类型的成员对象).

关于顺序是:按照C++类成员的定义顺序来初始化。

例如:

class A

{

int i;

int j;

};

A::A():i(j),j(100)

{

}

这样的初始化i得到未知的数字,不能得到100.

第七、STL容器是不是线程安全的?

第12条:切勿对 STL容器的线程安全性有不切实际的依赖。

STL的实现,是部分线程安全的。就是说,对容器和iostream,如果不同线程同时读同一容器对象线程安全。不同线程同时写同一容器的不同对象,线程安全。但不同线程同时读写同一对象,必须在外面自己做线程互斥和同步。

标准 C++的世界相当狭小和古旧。在这个纯净的世界中,所有的可执行程序都是静态链接的。不存在内存映像文件或共享内存。没有窗口系统,没有网络,没有数据库,也没有其他进程。考虑到这一点,当你得知 C++标准对线程只字未提时,你不应该感到惊讶。于是,你对 STL的线程安全性的第一个期望应该是,它会因不同实现而异。

当然,多线程程序是很普遍的,所以多数 STL提供商会尽量使自己的实现可在多线程环境下工作。然而,即使他们在这一方面做得不错,多数负担仍然在你的肩膀上。理解为什么会这样是很重要的。STL提供商对解决多线程问题只能做很有限的工作,你需要知道这一点。

在 STL容器中支持多线程的标准(这是多数提供商们所希望的)已经为 SGI所确定,并在它们的 STL Web站点[21]上发布。概括来说,它指出,对一个 STL实现你最多只能期望:

多个线程读是安全的。多个线程可以同时读同一个容器的内容,并且保证是正确的。自然地,在读的过程中,不能对容器有任何写入操作。

多个线程对不同的容器做写入操作是安全的。多个线程可以同时对不同的容器做写入操作。

就这些。我必须指明,这是你所能期望的,而不是你所能依赖的。有些实现提供了这些保证,有些则没有。

写多线程的代码并不容易,许多程序员希望 STL的实现能提供完全的线程安全性。如果是这样的话,程序员可以不必再考虑自己做同步控制。这无疑是很方便的,但要做到这一点将会很困难。考虑当一个库试图实现完全的容器线程安全性时可能采取的方式:

对容器成员函数的每次调用,都锁住容器直到调用结束。

在容器所返回的每个迭代器的生存期结束前,都锁住容器(比如通过 begin或 end调用)。

对于作用于容器的每个算法,都锁住该容器,直到算法结束(实际上这样做没有意义。因为,如同将在第 32条中解释的,算法无法知道它们所操作的容器。尽管如此,在这里我们仍要讨论这一选择。因为即便这是可能的,我们也会发现这种做法仍不能实现线程安全性,这对于我们的讨论是有益的)。

现在考虑下面的代码。它在一个 vector<int>中查找值为 5的第一个元素,如果找到了,就把该元素置为 0。

vector<int> v;

...

vector<int>::iterator first5(find(v.begin(), v.end(), 5)); //第 1行

if (first5 != v.end()){ //第 2行

*first5 = 0; //第 3行

}

在一个多线程环境中,可能在第 1行刚刚完成后,另一个不同的线程会更改 v中的数据。如果这种更改真的发生了,那么第 2行对 first5和 v.end是否相等的检查将会变得没有意义,因为 v的值将会与在第 1行结束时不同。事实上,这一检查会产生不确定的行为,因为另外一个线程可能会夹在第 1行和第 2行中间,使 first5变得无效,这第二个线程或许会执行一个插入操作使得 vector重新分配它的内存(这将会使 vector所有的迭代器变得无效。关于重新分配的细节,请参见第 14条)。类似地,第 3行对*first5的赋值也是不安全的,因为另一个线程可能在第
2行和第 3行之间执行,该线程可能会使 first5无效,例如可能会删除它所指向的元素(或者至少是曾经指向过的元素)。

上面所列出的加锁方式都不能防止这类问题的发生。第 1行中对 begin和 end的调用都返回得太快了,所以不会有任何帮助,它们生成的迭代器的生存期直到该行结束, find也在该行结束时返回。

上面的代码要做到线程安全, v必须从第 1行到第 3行始终保持在锁住状态,很难想象一个 STL实现能自动推断出这一点。考虑到同步原语(例如信号量、互斥体等)通常会有较高的开销,这就更难想象,一个 STL实现如何既能够做到这一点,同时又不会对那些在第 1行和第 3行之间本来就不会有另外线程来访问 v的程序(假设程序就是这样设计的)造成显著的效率影响。

这样的考虑说明了为什么你不能指望任何 STL实现来解决你的线程难题。相反,在这种情况下,必须手工做同步控制。在这个例子中,或许可以这样做:

想要使用STL时是线程安全的,需要自己处理而不是依赖STL的实现。可以手工做同步控制,如下面:

vector<int> v;

...

getMutexFor(v);

vector<int>::iterator first5(find(v.begin(),v.end(),5));

if(first5!=v.end())

{

*first5=0;

}

releaseMutexFor(v);

但是这种方法,可能忘了调用releaseMutexFor,这样这个互斥锁就永远也不会被释放。更为面向对象的方法是创建一个Lock类,它在构造函数中获得一个互斥体,在析构函数中释放它,从而尽可能减少getMutexFor调用,没有调用相应的releaseMutexFor调用的可能性。这样的类(实际上是一个类模板)看起来大概是:

template<typename Container>
class Lock

{
public:
Lock(const Container& container ):c(container)
{
   getMutexFor(c);
}
~Lock()
{
   ReleaseMutexFor(c);
}
private:
   const Container& c;
};

使用Lock:

vector<int>v;

...

{ //创建新的代码块

Lock<vector<int> >lock(v); //创建互斥体

vector<int>::iterator first5(find(v.begin(),v.end(),5));

if (first5!=v.end())

{

*first5=0;

}

} //代码块结束,自动释放互斥体

即使不创建新的代码块,在作用域结束,对象自动析构,只不过可能晚一点,但是如果是忘了调用releaseMutexFor,则永远不会释放互斥体。

因为 Lock对象在其析构函数中释放容器的互斥体,所以很重要的一点是,当互斥体应该被释放时,Lock就要被析构。为了做到这一点,我们创建了一个新的代码块( block),在其中定义了 Lock,当不再需要互斥体时就结束该代码块。看起来好像是我们把“调用 releaseMutexFor”这一任务换成了“结束代码块”,事实上这种说法是不确切的。如果我们忘了为 Lock创建新的代码块,则互斥体仍然会被释放,只不过会晚一些——当控制到达包含 Lock的代码块末尾时。而如果我们忘记了调用 releaseMutexFor,那么我们永远也不会释放互斥体。

而且,基于 Lock的方案在有异常发生时也是强壮的。 C++保证,如果有异常被抛出,局部对象会被析构,所以,即便在我们使用 Lock对象的过程中有异常抛出, Lock仍会释放它所拥有的互斥体 。如果我们依赖于手工调用 getMutexFor和 releaseMutexFor,那么,当在调用 getMutexFor之后而在调用 releaseMutexFor之前有异常被抛出时,我们将永远也无法释放互斥体。

异常和资源管理虽然很重要,但它们不是本条款的主题。本条款是讲述 STL中的线程安全性的。当涉及 STL容器和线程安全性时,你可以指望一个 STL库允许多个线程同时读一个容器,以及多个线程对不同的容器做写入操作。你不能指望 STL库会把你从手工同步控制中解脱出来,而且你不能依赖于任何线程支持。

. 已经证实存在一个漏洞。如果该异常根本没有被捕获到,那么程序将终止。在这种情况下,局部对象(如 lock)可能还没有让它们的析构函数被调用到。有些编译器会调用它们,有些编译器不会。这两种情况都是有效的。

第八、C++泛型编程

c++模板与泛型编程基础

泛型编程就是以独立于任何特定类型的方式编写代码,而模板是泛型编程的基础。

(1)定义函数模板(function template)

函数模板是一个独立于类型的函数,可以产生函数的特定类型版本。

// implement strcmp-like generic compare function

template <typename T>

int compare(const T &v1, const T &v2)

{

if (v1 < v2) return -1;

if (v2 < v1) return 1;

return 0;

}

模板定义以关键字template开始,后接尖括号括住的模板形参表。

模板形参可以是表示类型的类型形参(type parameter),也可以是表示常量表达式的非类型形参(nontype parameter)。上面程序中的T是类型形参。

// compiler instantiates int compare(const int&, const int&)

cout << compare(1, 0) << endl;

// compiler instantiates int compare(const string&, const string&)

string s1 = “hi”, s2 = “world”;

cout << compare(s1, s2) << endl;

使用函数模板时,编译器会将模板实参绑定到模板形参。编译器将确定用什么类型代替每个类型形参,用什么值代替每个非类型形参,然后产生并编译(称为实例化)该版本的函数。

上面的例子中,编译器用int代替T创建第一个版本,用string代替T创建第二个版本。

函数模板也可以声明为inline。

// inline specifier follows template parameter list

template <typename T> inline T min(const T&, const T&);

(2)定义类模板(class template)

在定义的类模板中,使用模板形参作为类型或值的占位符,在使用类时再提供具体的类型或值。

template <typename Type>

class Queue

{

public:

Queue();

Type & front();

const Type & front() const;

void push(const Type &);

void pop();

bool empty() const;

private:

// …

};

与调用函数模板不同,使用类模板时,必须为模板形参显示指定实参。

Queue<int> qi; // Queue that holds ints

Queue<string> qs; // Queue that holds strings

(3)模板类型形参

类型形参由关键字class或typename后接说明符构成。在函数模板形参表中,二者含义相同。typename其实比class更直观,更清楚的指明后面的名字是一个类型名(包括内置类型),而class很容易让人联想到类声明或类定义。

此外,在使用嵌套依赖类型(nested depended name)时,必须用到typename关键字。

在类的内部可以定义类型成员。如果要在函数模板内部使用这样的类型,必须显示告诉编译器这个名字是一个类型,否则编译器无法得知它是一个类型还是一个值。默认情况下,编译器假定这样的名字指定(静态)数据成员,而不是类型。所以下面这段程序,如果去掉typename关键字,将会出现编译错误。

template <typename Parm, typename U>

Parm fcn(Parm *array, U value)

{

typename Parm::size_type * p;

}

(4)非类型模板形参

模板形参也可以是非类型形参,在使用时非类型形参由常量表达式代替。

// initialize elements of an array to zero

template <typename T, size_t N>

void array_init(T (&parm)
)

{

for (size_t i = 0; i != N; ++i)

parm[i] = 0;

}



int x[42];

double y[10];

array_init(x); // instantiates array_init(int (&)[42])

array_init(y); // instantiates array_init(double (&)[10])

(5)编写泛型程序

模板代码需要对使用的类型做一些假设,比如上面的compare()要求类型T重载了“<”操作符。所以函数模板内部完成的操作就限制了可用于实例化该函数的类型。

编写模板代码时,对实参类型的要求应尽可能少。比如compare()函数仅使用了“<”操作符,而没有使用“>”操作符。

第九、引用和指针的区别

1.从现象上看:指针在运行时可以改变其所指向的值,而引用一旦和某个对象绑定后就不再改变

2.从内存分配上看:程序为指针变量分配内存区域,而引用不分配内存区域

3.从编译上看:程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改。

第十、C中不安全的函数

C里操作字符串很高效,但也很麻烦。

1. char * strcpy ( char * destination, const char * source );

最常用的函数,但是却不安全,原因在于,一是要destination有足够的空间,二是要保证source和destination指向的空间没有overlap。

2. int sprintf ( char * str, const char * format, ... );

也许要问,这个怎么用于字符串拷贝呢?可以这么用 sprintf(dest, "%s", src); 但是要调用者保证dest有足够的内存存放src。

3. char * strncpy ( char * destination, const char * source, size_t num );

比起strcpy,多了个长度的控制。从source拷贝num个字符到destination。如果source里不够num字符怎么办呢?会补充0。

一个典型的用法是:

char buf[MAX];

strncpy(buf, src, MAX-1);

这段代码的本意是,一个长为MAX的buf,最多也就放MAX-1个字符,最后一个位置放‘\0'。因此最多能从src里拷贝MAX-1个字符,如果src里没这么多,剩余的填充0就是了。

但是这样做就安全了么?不是,如果src刚好MAX-1个字符。注意到strncpy只复制了MAX-1个字符,最后一个位置未知,有潜在的隐患。下段代码可以诠释:

#define MAX 4

char buf[MAX];

char* src="123";

// solution 1. memset(buf, 0, MAX);

strncpy(buf, src, MAX-1);

// solution 2. buf[MAX-1] = '\0';

printf("%s\n", buf);

有两个办法可以解决:

1. 调用strncpy之前memset为0,有点浪费。

2. 在strncpy之后对最后一个字符赋值为0。都可以,但不够优雅。

4. int snprintf( char *buffer, int buff_size, const char *format, ... );

用作字符串拷贝的用法:

char buf[MAX];

snprintf(buf, sizeof(buf), "%s", src);

即安全,又简洁。

你可能会关心:如果src的长度大于dest(buf)呢?这个是另外一个问题,这里需要的是安全的字符串拷贝,在C语言里,如果一个字符串指针指向的内存没有结尾字符'\0',是非常危险的。

snprintf会把buf的最后一个位置保留为'\0'。

关于返回值:如果当前buf够用,返回实际写入的字符数;如果不够用,返回将要写入的字符数。换句话说,返回值就是传入的字符数目。

假设当前的buf[4].

待写入 实际写入 返回值

12 12 2 够用

123 123 3 够用

1234 123 4 不够用

12345 123 5 不够用

sprintf/snprintf的另外一个用法:

itoa不是ANSI C或C++的一部分,可以变相的用sprintf来代替:

sprintf(str,"%d",value) 转换为十进制数值。

sprintf(str,"%x",value) 转换为十六进制数值。

sprintf(str,"%o",value) 转换为八进制数值。

总结:

做边界检查,防止溢出;小心的把一个大的数据块传递给一个函数;

C大多数注重的是效率,但是程序员对于安全性问题要保持警惕。

微软函数库对C标准函数库的改进。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: