剑指XX游戏(九) - C/C++必知必会

C语言部分

1.指针辨析

int (*p)[5]和int *p[5]

前者是一个指针,它指向一个含有5个元素的数组。后者是一个数组,它的长度为5,数组中每一个元素指向一个整型变量。

int *f( int i, int j)和 int (*p)( int i ,int j)

前者是返回指针的函数,它是一个函数的声明,后者是指向函数的指针,它定义了一个指针。


2.C语言static的作用。

? 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。

? 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访 问,但不能被模块外其它函数访问。它是一个本地的全局变量。 

? 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是 ,这个函数被限制在声明它的模块的本地范围内使用。


3.extern "C" 的作用。

实现C++与C及其它语言的混合编程.

以int func(int, int)为例,C的编译器会将名字改编为_func, 而C++的编译器会改编为_func_int_int 或_funcii(各编译器不同)。

如果这个函数在C中编译成库,目标文件中函数名为_func,当这个函数中C++中被调用时,C++的编译器就会到目标文件中寻找_funcii,结果找不到,出错。
所以为了防止这种问题,在C++调用时,将函数声明前加个extern "C" 告诉C++的编译器,不要对名字再进修饰,而直接去找_func。


4.strcpy,strncpy的区别。

都是字符串的拷贝函数,首先看函数原型

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

src和dest所指内存区域不可以重叠且,dest必须有足够的空间来容纳src的字符串。返回指向dest的指针。

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

第三个参数表示从source处拷贝的最大字符串数。

把src所指由‘\0‘结束的字符串的前n个字节复制到dest所指的数组中。

如果src的前n个字节不含NULL字符,则结果不会以NULL字符结束。
如果src的长度小于n个字节,则以NULL填充dest直到复制完n个字节。
src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
返回指向dest的指针。


5.手动实现strcpy。

char * strcpy ( char * destination, const char * source )
{
	if(destination == NULL || source == NULL)
		return NULL;
	char *p = destination;
		
	while((*destination++ = *source++) != ‘\0‘);
	return p;	 
}



C++语言部分

1.指针和引用的区别

1). 指针是一个实体,而引用仅是个别名;
2). 引用使用时无需解引用(*),指针需要解引用;
3). 引用只能在定义时被初始化一次,之后不可变;指针可变;
4). 引用没有 const,指针有 const;
5). 引用不能为空,指针可以为空;
6). “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
7). 指针和引用的自增(++)运算意义不一样;
8).从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。)


2.拷贝构造函数在什么时候被调用,什么是深浅拷贝。

一个对象以值传递的方式传入函数体 。
一个对象以值传递的方式从函数返回 。
一个对象需要通过另外一个对象进行初始化。

       在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

       浅拷贝指的是在对象复制时,只是对对象中的数据成员进行简单的赋值,上面的例子都是属于浅拷贝的情况,默认拷贝构造函数执行的也是浅拷贝。

       深拷贝的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间。


3.String 类的实现.

String.cpp

#include <utility> //For std::swap
#include <string.h> //For strcpy

class String
{
	public:
	String():m_data(new char[1])
	{
		*m_data = ‘\0‘;
	}
	
	String(const char* str)
	{
		if(str == NULL)
		{
			m_data = new char[1];
			*m_data = ‘\0‘;
		}
		else
		{
			m_data = new char[strlen(str) + 1];
			strcpy(m_data, str);
		}
	}
	String(const String &rhs)
	{
		if(rhs == NULL)
		{
			m_data = new char[1];
			*m_data = ‘\0‘;
		}
		else
		{
			m_data = new char[rhs.size() + 1];
			strcpy(m_data, rhs.c_str());
		}
	}
	
	~String()
	{
		delete[] m_data;
	}
	
	size_t size() const
	{
		return strlen(m_data);
	}
	
	const char* c_str() const
	{
		return m_data;
	}
	
	String& operator=(const String &rhs)
	{
		if(this == other)
		return *this;
		
		delete[] m_data;
		m_data = new char[rhs.size() + 1];
		strcpy(m_data, rhs.c_str());
		return *this;
	}
	
	private:
	char* m_data;
}


4.STL中各种容器底层实现和特点及使用场合.

vector 底层数据结构为数组 ,支持快速随机访问
list 底层数据结构为双向链表,支持快速增删
deque 底层数据结构为一个中央控制器和多个缓冲区,支持首尾(中间不能)快速增删,也支持随机访问
stack 底层一般用23实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
queue 底层一般用23实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时
45是适配器,而不叫容器,因为是对容器的再封装
priority_queue 的底层数据结构一般为vector为底层容器,堆heap为处理规则来管理底层容器实现
set 底层数据结构为红黑树,有序,不重复
multiset 底层数据结构为红黑树,有序,可重复
map 底层数据结构为红黑树,有序,不重复
multimap 底层数据结构为红黑树,有序,可重复
hash_set 底层数据结构为hash表,无序,不重复
hash_multiset 底层数据结构为hash表,无序,可重复
hash_map 底层数据结构为hash表,无序,不重复
hash_multimap 底层数据结构为hash表,无序,可重复


vector、list和deque提供给程序员不同的复杂度,因此应该这么用:vector是一种可以默认使用的序列类型,当很频繁地对序列中部进行插入和删除时应该用list,当大部分插入和删除发生在序列的头或尾时可以选择deque这种数据结构。

你需要“可以在容器的任意位置插入一个新元素”的能力吗?如果是,你需要序列容器,关联容器做不到。

你关心元素在容器中的顺序吗?如果不,散列容器就是可行的选择。否则,你要避免使用散列容器。

你需要哪一类迭代器?如果必须是随机访问迭代器,在技术上你就只能限于vector、deque和string。

当插入或者删除数据时,是否非常在意容器内现有元素的移动?如果是,你就必须放弃连续内存容器。

容器中的数据的内存布局需要兼容C吗?如果是,你就只能用vector。

查找速度很重要吗?如果是,你就应该看看散列容器(优于)排序的vector(优于)标准的关联容器大概是这个顺序。

你需要插入和删除的事务性语义吗?也就是说,你需要有可靠地回退插入和删除的能力吗?如果是,你就需要使用基于节点的容器。如果你需要多元素插入的事务性语义,你就应该选择list,因为list是唯一提供多元素插入事务性语义的标准容器。事务性语义对于有兴趣写异常安全代码的程序员来说非常重要。

你要把迭代器、指针和引用的失效次数减到最少吗?如果是,你就应该使用基于节点的容器,因为在这些容器上进行插入和删除不会使迭代器、指针和引用失效(除非它们指向你删除的元素)。一般来说,在连续内存容器上插入和删除会使所有指向容器的迭代器、指针和引用失效。

你需要具有以下特性的序列容器吗:1)可以使用随机访问迭代器;2)只要没有删除而且插入只发生在容器结尾,指针和引用的数据就不会失效?这个一个非常特殊的情况,但如果你遇到这种情况,deque就是你梦想的容器。(有趣的是,当插入只在容器结尾时,deque的迭代器也可能会失效,deque是唯一一个“在迭代器失效时不会使它的指针和引用失效”的标准STL容器。)


5.函数前的const和函数后的const

前面使用const 表示返回值为const,如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值。如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
后面加 const是const成员函数,在函数定义和函数声明中都要加const.

const数据成员和const成员函数

const修饰类的数据成员,表示成员常量,不能被修改,同时它只能在初始化列表中赋值。

const成员函数应该在函数原型说明和函数定义中都增加const限定。

对于const类对象/指针/引用,只能调用类的const成员函数,因此,const修饰成员函数的最重要作用就是限制对于const对象的使用。

const成员函数不被允许修改它所在对象的任何一个数据成员。

const修饰类对象表示该对象为常量对象,其中的任何成员都不能被修改。对于对象指针和对象引用也是一样。


const的用法

· 要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
· 要避免最一般的赋值操作错误,如将const变量赋值;
· 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
· const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
· 不要轻易的将函数的返回值类型定为const;
· 除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;
· 任何不会修改数据成员的函数都应该声明为const 类型。


6.C++中四种类型转换符。

const_cast,字面上理解就是去const属性。
static_cast,命名上理解是静态类型转换。如int转换成char。
dynamic_cast,命名上理解是动态类型转换。如子类和父类之间的多态类型转换。
reinterpreter_cast,仅仅重新解释类型,但没有进行二进制的转换。


7.volitale关键字

如果一个基本变量被volatile修饰,编译器将不会把它保存到寄存器中,而是每一次都去访问内存中实际保存该变量的位置上。这一点就避免了没有volatile修饰的变量在多线程的读写中所产生的由于编译器优化所导致的灾难性问题。所以多线程中必须要共享的基本变量一定要加上volatile修饰符。当然了,volatile还能让你在编译时期捕捉到非线程安全的代码。


8.explicit关键字

explicit 可以有效得防止构造函数的隐式转换带来的错误或者误解。


9.说一下static关键字的用法。

static 关键字可用于声明变量、函数、类数据成员和类函数,主要用来控制变量的存储方式和可见性。

在类里面

类体中的数据成员的声明前加上static关键字,该数据成员就成为了该类的静态数据成员。和其他数据成员一样,静态数据成员也遵守public/protected/private访问规则。同时,静态数据成员:
1).静态数据成员实际上是类域中的全局变量。所以,静态数据成员的定义(初始化)不应该被放在头文件中;

2).静态数据成员被 类 的所有对象所共享,包括该类派生类的对象。即派生类对象与基类对象共享基类的静态数据成员;

3).静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以;

4).★静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为 所属类类型的 指针或引用;

5).★const函数中可以改变类的static成员。不能改变普通成员;

#include <iostream>
using namespace std;

class base{
public:
    base(){_i=0;_val=0;}

    mutable int _i;
    static int _staticVal;
    int _val;
    void test() const{//const function member
        _i++;//right,mutable member
        _staticVal++;//right,static member
        _val++;//error: increment of member ‘base::_val’ in read-only object
    }
};

int base::_staticVal=0;
int main()
{
    cout<<"Hello!"<<endl;
    return 0;
}


静态成员函数:

静态成员函数是为类的全体对象服务的即用事例化该类就可以访问静态成员函数了。这样就不需要定义一个全局函数了,有下面的特点

1).静态成员函数的地址可用普通函数指针储存,而普通成员函数地址需要用 类成员函数指针来储存;

2).静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针;

3).静态成员函数不可以同时声明为 virtual、const、volatile函数;


内存分配

类的静态成员变量和函数中的静态变量一样,在编译的时候就分配内存了,存放在data段,直到程序退出才释放,并不是随着对象的删除而释放的。


在C语言中,static有一点稍微的不同。

static 变量:
1).变量会被放在程序的全局存储区中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
2).变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。

static函数

1).静态函数会被自动分配在一个一直使用的存储区,直到退出应用程序实例,避免了调用函数时压栈出栈,速度快很多。

2).static函数(也叫内部函数)只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用。



郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。