C++基础知识

C++基础知识

概述

C与C++的区别

1.C是面向过程的语言,C++是面向对象的语言
2.C和C++动态管理内存的方法不一样,C是使用malloc/free函数,而C++除此之外还有new/delete关键字;;
3.C中的struct和C++的类,C++的类是C所没有的,但是C中的struct是可以在C++中正常使用的,并且C++对struct进行了进一步的扩展,使struct在C++中可以和class一样当做类使用,而唯一和class不同的地方在于struct的成员默认访问修饰符是public,而class默认的是private;
4.C++支持函数重载,而C不支持函数重载,而C++支持重载的依仗就在于C++的名字修饰与C不同,例如在C++中函数int fun(int ,int)经过名字修饰之后变为 _fun_int_int ,而C是 _fun,一般是这样的,所以C++才会支持不同的参数调用不同的函数;
5.C++中有引用,而C没有;
6.C++全部变量的默认链接属性是外链接,而C是内连接;
7.C 中用const修饰的变量不可以用在定义数组时的大小,但是C++用const修饰的变量可以(如果不进行&,解引用的操作的话,是存放在符号表的,不开辟内存);
8.局部变量的声明规则不同,多态,C++特有输入输出流之类的,很多,下面就不再列出来了

C库函数的实现

strcpy

源字符串src的字符指针范围为[src,src+count-1]
(判断技巧:当count为1时,此时的范围为[src,src]
1.当目标字符串dest的范围为(0,src][src+count,+无穷)时,直接从当前位置开始拷贝即可
拷贝方向:从低字节到高字节
2.否则:
让src指向src的最后一个位置
dest也加上同样的偏移量
拷贝方向为:从高字节到低字节

// strcpy的实现
char * strcpy(char * dest, const char * src){
	assert(dest && src);
	char * ret = dest;
	
	// 内存拷贝的字节数
	int count = strlen(src) + 1;
	// 内存拷贝
	if(dest <= src || (dest >= src + count)){
		// 拷贝方向:从低字节到高字节
		while(count--){
			*dest++ = *src++;
		}
	}else{
		// 拷贝方向:从高字节向低字节
		dest += (count -1);
		src += (count -1);
		while(count--){
			*dest--=*src--;
		}
	}
	return ret;
}
memcpy

和上面的逻辑相同,注意void*在处理时要先转换为char*

void* memcpy(void* dest,void* src,size_t n){
    assert(dest != NULL && src != NULL);

    if(src < dest && (char*)src + n > dest){
        char* pdst = (char*)dest+n-1;
        char* psrc = (char*)src+n-1;
        while(n--){
            *pdst-- = *psrc--;
        }
    }else{
        char* pdest = (char*)dest;
        char* psrc = (char*)src;
        while(n--){
            *pdest++ = *psrc++;
        }
    }
    return dest;
}
strcat
// strcat的实现
char * strcat(char * dest, const char * src){
	assert(dest && src);
	char * ret = dest;
	// 找到dest的'\0'结尾符
	while(*dest){
		dest++;
	}
	// 拷贝(while循环退出时,将结尾符'\0'也做了拷贝)
	while(*dest++ = *src++){}
	return ret;
}
strcmp

注意要返回的是第一个不同字符的差值

// strcmp的实现
int strcmp(const char * str1, const char * str2){
	assert(str1 && str2);
	// 找到首个不相等的字符
	while(*str1 && *str2 && (*str1==*str2)){
		str1++;
		str2++;
	}
	return *str1 - *str2;
}
strlen
size_t strlen(const char* src){
    assert(src);
    size_t ret = 0;
    while(*src++ != '\0'){
        ret++;
    }
    return ret;
}
memset
void* memset(void* src,int c,size_t n){
    assert(src != NULL);
    char* psrc = (char*)src;
    while(n--){
        *psrc++ = (char)c;
    }
    return src;
}
atoi
malloc&&free
#define NALLOC 1024
#define NULL 0
typedef long Align;
union header {
	struct {
		union header *ptr;//指向下一块空闲快
		unsigned size;//当前空闲块的大小
		} s;
	Align x;//x主要是保证分配的内存为Align的倍数。涉及到内存对齐
};
 
typedef union header Header;
static Header base;
static Header *freep=NULL;
static Header *morecore(unsigned);
 
void *mymalloc(unsigned nbytes)
{
	Header *p,*prevp;
       
	unsigned nunites;
	nunites =(nbytes+sizeof(Header)-1)/sizeof(Header)+1;//分配的内存大小
	if((prevp =freep)==NULL){//no free list yet,the first call malloc
		base.s.ptr =freep=prevp=&base;
		base.s.size=0;
		}
	for(p=prevp->s.ptr; ;prevp=p,p=p->s.ptr){
		if(p->s.size>=nunites){ //big enough
			if(p->s.size==nunites)//exactly
			prevp->s.ptr=p->s.ptr;
			else{//allocate tail end总是从每块空闲快的尾部截取
				p->s.size-=nunites;
				p+=p->s.size;
				p->s.size=nunites;
				}
			freep=prevp;
			return (void *)(p+1);
		}
		if(p==freep){
			if((p=morecore(nunites))==NULL)
			return NULL;
		}
}
}
 
void myfree(void *ap)
{
	Header *bp,*p;
	bp=(Header*)ap-1;
	for(p=freep;!(bp>p&&bp<p->s.ptr);p=p->s.ptr)
		if(p>=p->s.ptr && (bp>p||bp<p->s.ptr))
			break;
	if(bp+bp->s.size==p->s.ptr){
		bp->s.size+=p->s.ptr->s.size;
		bp->s.ptr=p->s.ptr->s.ptr;//够绕的
		}
	else{
		bp->s.ptr=p->s.ptr;
		}
	if(p+p->s.size==bp){
		p->s.size+=bp->s.size;
		p->s.ptr=bp->s.ptr;
		}
	else
		p->s.ptr=bp;
	freep=p;
}
static Header *morecore(unsigned nu)
{
	char *cp, *sbrk(int);//linux系统调用,扩展堆空间
	Header *up;
	if(nu<NALLOC)
		nu=NALLOC;
	cp=sbrk(nu*sizeof(Header));
	if(cp==(char*)-1)
		return NULL;
	up=(Header*)cp;
	up->s.size=nu;
	myfree((void *)(up+1));
	return freep;
}	
 
memset、memcpy和strcpy的区别

memcpy是内存拷贝函数,可以拷贝任何数据类型的对象;
例如memcpy(b, a, sizeof(b))

strcpy只能拷贝字符串,遇到’\0′结束拷贝。

memset用来对一段内存空间全部设置为某个字符;

例如:
char a[100];
memset(a, '', sizeof(a))。

string

关键代码:

str=new char[strlen(cstr)+1];
strcpy(str,cstr);

注意细节:
1.构造函数:传入参数可能为空
2.重载=复制构造函数:
自我复制的情况
先删掉原来的string

class String{
	public:
	String(const char* cstr=0);
	String(const String& str);
	String& operator =(const String &str);
	~String();
	char *get_c_str()const{
		return str;
	}
	private:
		char *str;
} 

inline String::String(const char* cstr=0){
	if(ctr){
	str=new char[strlen(cstr)+1];
	strcpy(str,cstr);
	}
	else{
	str=new char[1];
	*str=/0;
	}
}

inline String::~String(){
	delete[] str;
}
inline String::String(const String&str){
	str=new char[strlen(cstr)+1];
	strcpy(str,cstr);
}
inline String& String::operator =(const String &cstr){
	if(this==&cstr) return *this;
	delete[] str;
	str=new char[strlen(cstr.str)+1];
	strcpy(str,cstr);
	return *this;
}
string类提供的方法

标准库的string类提供了3个成员函数来从一个string得到c类型的字符数组:
c_str()data()copy(p,n)

c_str()生成一个const char*指针,指向以空字符终止的数组。

注:
①这个数组的数据是临时的,当有一个改变这些数据的成员函数被调用后,其中的数据就会失效。因此要么现用先转换,要么把它的数据复制到用户自己可以管理的内存中。注意。看下例:

const char* c;
string s=“1234”;
c = s.c_str();
cout<<c<<endl; //输出:1234
s=“abcd”;
cout<<c<<endl; //输出:abcd

上面如果继续用c指针的话,导致的错误将是不可想象的。就如:1234变为abcd
  其实上面的c = s.c_str(); 不是一个好习惯。既然c指针指向的内容容易失效,我们就应该按照上面的方法,那怎么把数据复制出来呢?这就要用到strcpy等函数(推荐)。
代码实现如下:

#include <iostream>
#include <string>
#include <exception>
using namespace std;


int main () 
{
    char a[5]={'1','2','3','4',0};
    string s="2468";
    char b[5];   //b数组长度>=a数组长度 b数组长度>=s字符串长度+1

    strcpy(b,a);      //字符串数组copy到另一个字符串数组
    cout<<b<<endl;

    string s1(b,b+2);    //字符串数组->string 并且可以指定位置  前闭后开
    cout<<s1<<endl;

    strcpy(b,s.c_str());  //string->字符串数组
    cout<<b<<endl;

    string s2(s1.substr(0,2));    //string ->string  指定位置
    cout<<s2<<endl;

    string s3(s2.c_str());    //s2转换为字符数组  再赋值给string
    cout<<s3<<endl;

    string s4(s3);           //直接初始化
    cout<<s4<<endl;

    return 0;

}

② c_str()返回一个客户程序可读不可改的指向字符数组的指针,不需要手动释放或删除这个指针。

2、data():与c_str()类似,但是返回的数组不以空字符终止

3、copy(p,n,size_type _Off = 0)
从string类型对象中至多复制n个字符到字符指针p指向的空间中。默认从首字符开始,但是也可以指定,开始的位置(记住从0开始)。返回真正从对象中复制的字符。——用户要确保p指向的空间足够保存n个字符。

设计智能指针

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

关键字

volatile作用

Volatile关键词的第一个特性:易变性。所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。

Volatile关键词的第二个特性:“不可优化”特性。volatile告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。

Volatile关键词的第三个特性:”顺序性”,能够保证Volatile变量间的顺序性,编译器不会进行乱序优化。

C/C++ Volatile变量,与非Volatile变量之间的操作,是可能被编译器交换顺序的。C/C++ Volatile变量间的操作,是不会被编译器交换顺序的。哪怕将所有的变量全部都声明为volatile,哪怕杜绝了编译器的乱序优化,但是针对生成的汇编代码,CPU有可能仍旧会乱序执行指令,导致程序依赖的逻辑出错,volatile对此无能为力

针对这个多线程的应用,真正正确的做法,是构建一个happens-before语义。

static

控制变量的存储方式和可见性

(1)修饰局部变量

一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果用static进行修饰的话,该变量便存放在静态数据区,其生命周期一直持续到整个程序执行结束。但是在这里要注意的是,虽然用static对局部变量进行修饰过后,其生命周期以及存储空间发生了变化,但是其作用域并没有改变,其仍然是一个局部变量,作用域仅限于该语句块。

(2)修饰全局变量

对于一个全局变量,它既可以在本源文件中被访问到,也可以在同一个工程的其它源文件中被访问(只需用extern进行声明即可)。用static对全局变量进行修饰改变了其作用域的范围,由原来的整个工程可见变为本源文件可见

(3)修饰函数

用static修饰函数的话,情况与修饰全局变量大同小异,就是改变了函数的作用域。只能处理静态数据

(4)C++中的static

如果在C++中对类中的某个函数用static进行修饰,则表示该函数属于一个类而不是属于此类的任何特定对象;如果对类中的某个变量进行static修饰,表示该变量为类以及其所有的对象所有。它们在存储空间中都只存在一个副本。可以通过类和对象去调用

const的含义及实现机制

const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动

(1)const修饰基本数据类型

1.const修饰一般常量及数组

基本数据类型,修饰符const可以用在类型说明符前,也可以用在类型说明符后,其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值便好。

2.const修饰指针变量*及引用变量&

如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;

如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。

(2)const应用到函数中,

1.作为参数的const修饰符

调用函数的时候,用相应的变量初始化const常量,则在函数体中,按照const所修饰的部分进行常量化,保护了原对象的属性
[注意]:参数const通常用于参数为指针或引用的情况;

2.作为函数返回值的const修饰符

声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用

(3)const在类中的用法

不能在类声明中初始化const数据成员。正确的使用const实现方法为:const数据成员的初始化只能在类构造函数的初始化表中进行
类中的成员函数:A fun4()const; 其意义上是不能修改所在类的的任何变量

(4)const修饰类对象,定义常量对象
常量对象只能调用常量函数,别的成员函数都不能调用。

http://www.cnblogs.com/wintergrass/archive/2011/04/15/2015020.html

extern

在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

注意extern声明的位置对其作用域也有关系如果是在main函数中进行声明的,则只能在main函数中调用,在其它函数中不能调用。其实要调用其它文件中的函数和变量,只需把该文件用#include包含进来即可,为啥要用extern?因为用extern会加速程序的编译过程,这样能节省时间

在C++中extern还有另外一种作用,用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题

define&&inline

内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。 宏定义不检查函数参数,返回值什么的,只是展开,相对来说,内联函数会检查参数类型,所以更安全。 内联函数和宏很类似,而区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销

宏是预编译器的输入,然后宏展开之后的结果会送去编译器做语法分析。宏与函数等处于不同的级别,操作不同的实体。宏操作的是 token, 可以进行 token的替换和连接等操作在语法分析之前起作用。而函数是语言中的概念,会在语法树中创建对应的实体,内联只是函数的一个属性

对于问题:有了函数要它们何用?答案是:
一:函数并不能完全替代宏,有些宏可以在当前作用域生成一些变量,函数做不到
二:内联函数只是函数的一种,内联是给编译器的提示,告诉它最好把这个函数在被调用处展开,省掉一个函数调用的开销(压栈,跳转,返回)

内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样

内联函数必须是和函数体申明在一起,才有效

宏定义和内联函数区别

sizeof运算符与strlen函数的区别

1)strlen计算字符串的具体长度(只能是字符串),不包括字符串结束符。返回的是字符个数。
2)sizeof计算声明后所占的内存数(字节大小),不是实际长度。
3)sizeof是一个取字节运算符,而strlen是个函数
4)sizeof的返回值=字符个数*字符所占的字节数
字符实际长度小于定义的长度,此时字符个数就等于定义的长度。
若未给出定义的大小,分类讨论:
对于字符串数组,字符大小等于实际的字符个数+1;
对于整型数组,字符个数为实际的字符个数。
字符串每个字符占1个字节,整型数据每个字符占的字节数需根据系统的位数类确定,32位占4个字节。
5)sizeof可以用类型做参数,strlen只能用char做参数,且必须以‘\0’结尾,sizeof还可以用函数*做参数;
6)数组做sizeof的参数不退化,传递给strlen就退化为指针

8.new,delete的实现
new:operator new(内部调用malloc);static_cast;调用构造函数;
delete:调用析构函数;释放内存
array new一定要array delete

9.什么是函数模板?
template函数的参数类型是模板,编译器会对函数模板进行参数推导
STL原理及实现:
STL各类型容器实现,STL共有六大组件
STL提供六大组件,彼此可以组合套用:
1、容器(Containers):各种数据结构,如:序列式容器vector、list、deque、关联式容器set、map、multiset、multimap。用来存放数据。从实现的角度来看,STL容器是一种class template。

2、算法(algorithms):各种常用算法,如:sort、search、copy、erase。从实现的角度来看,STL算法是一种 function template。注意一个问题:任何的一个STL算法,都需要获得由一对迭代器所标示的区间,用来表示操作范围。这一对迭代器所标示的区间都是前闭后开区间,例如[first, last)

3、迭代器(iterators):容器与算法之间的胶合剂,是所谓的“泛型指针”。共有五种类型,以及其他衍生变化。从实现的角度来看,迭代器是一种将 operator*、operator->、operator++、operator- - 等指针相关操作进行重载的class template。所有STL容器都有自己专属的迭代器,只有容器本身才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。

4、仿函数(functors):行为类似函数,可作为算法的某种策略(policy)。从实现的角度来看,仿函数是一种重载了operator()的class或class template。一般的函数指针也可视为狭义的仿函数。

5、配接器(adapters):一种用来修饰容器、仿函数、迭代器接口的东西。例如:STL提供的queue 和 stack,虽然看似容器,但其实只能算是一种容器配接器,因为它们的底部完全借助deque,所有操作都由底层的deque供应。改变 functors接口者,称为function adapter;改变 container 接口者,称为container adapter;改变iterator接口者,称为iterator adapter。

6、配置器(allocators):负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。

这六大组件的交互关系:container(容器) 通过 allocator(配置器) 取得数据储存空间,algorithm(算法)通过 iterator(迭代器)存取 container(容器) 内容,functor(仿函数) 可以协助 algorithm(算法) 完成不同的策略变化,adapter(配接器) 可以修饰或套接 functor(仿函数)

序列式容器:
vector-数组,元素不够时再重新分配内存,拷贝原来数组的元素到新分配的数组中。
list-单链表。
deque-分配中央控制器map(并非map容器),map记录着一系列的固定长度的数组的地址.记住这个map仅仅保存的是数组的地址,真正的数据在数组中存放着.deque先从map中央的位置(因为双向队列,前后都可以插入元素)找到一个数组地址,向该数组中放入数据,数组不够时继续在map中找空闲的数组来存数据。当map也不够时重新分配内存当作新的map,把原来map中的内容copy的新map中。所以使用deque的复杂度要大于vector,尽量使用vector。

stack-基于deque。
queue-基于deque。
heap-完全二叉树,使用最大堆排序,以数组(vector)的形式存放。
priority_queue-基于heap。
slist-双向链表。

关联式容器:
set,map,multiset,multimap-基于红黑树(RB-tree),一种加上了额外平衡条件的二叉搜索树。

hash table-散列表。将待存数据的key经过映射函数变成一个数组(一般是vector)的索引,例如:数据的key%数组的大小=数组的索引(一般文本通过算法也可以转换为数字),然后将数据当作此索引的数组元素。有些数据的key经过算法的转换可能是同一个数组的索引值(碰撞问题,可以用线性探测,二次探测来解决),STL是用开链的方法来解决的,每一个数组的元素维护一个list,他把相同索引值的数据存入一个list,这样当list比较短时执行删除,插入,搜索等算法比较快。

hash_map,hash_set,hash_multiset,hash_multimap-基于hashtable。
[STL六大组件] (http://blog.csdn.net/chenguolinblog/article/details/30336805)
什么是“标准非STL容器”?
list和vector有什么区别?
vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,因此不支持随机存取,如果需要大量的插入和删除,而不关心随即存取,则应使用list。
虚函数:
1.虚函数的作用和实现原理,什么是虚函数,有什么作用?
C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚函数来体现的。

虚函数实现原理:包括虚函数表、虚函数指针等

虚函数的作用说白了就是:当调用一个虚函数时,被执行的代码必须和调用函数的对象的动态类型相一致。编译器需要做的就是如何高效的实现提供这种特性。不同编译器实现细节也不相同。大多数编译器通过vtbl(virtual table)和vptr(virtual table pointer)来实现的。 当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的vtbl。vtbl实际上就是一个函数指针数组,有的编译器用的是链表,不过方法都是差不多。vtbl数组中的每一个元素对应一个函数指针指向该类的一个虚函数,同时该类的每一个对象都会包含一个vptr,vptr指向该vtbl的地址。
结论:
每个声明了虚函数或者继承了虚函数的类,都会有一个自己的vtbl
同时该类的每个对象都会包含一个vptr去指向该vtbl
虚函数按照其声明顺序放于vtbl表中, vtbl数组中的每一个元素对应一个函数指针指向该类的虚函数
如果子类覆盖了父类的虚函数,将被放到了虚表中原来父类虚函数的位置???
在多继承的情况下,每个父类都有自己的虚表。子类的成员函数被放到了第一个父类的表中??
2.衍生问题:为什么 C++里访问虚函数比访问普通函数慢?
单继承时性能差不多,多继承的时候会慢
调用性能方面
从前面虚函数的调用过程可知。当调用虚函数时过程如下(引自More Effective C++):

通过对象的 vptr 找到类的 vtbl。这是一个简单的操作,因为编译器知道在对象内 哪里能找到 vptr(毕竟是由编译器放置的它们)。因此这个代价只是一个偏移调整(以得到 vptr)和一个指针的间接寻址(以得到 vtbl)。
找到对应 vtbl 内的指向被调用函数的指针。这也是很简单的, 因为编译器为每个虚函数在 vtbl 内分配了一个唯一的索引。这步的代价只是在 vtbl 数组内 的一个偏移。
调用第二步找到的的指针所指向的函数。
在单继承的情况下,调用虚函数所需的代价基本上和非虚函数效率一样,在大多数计算机上它多执行了很少的一些指令,所以有很多人一概而论说虚函数性能不行是不太科学的。在多继承的情况下,由于会根据多个父类生成多个vptr,在对象里为寻找 vptr 而进行的偏移量计算会变得复杂一些,但这些并不是虚函数的性能瓶颈。 虚函数运行时所需的代价主要是虚函数不能是内联函数。这也是非常好理解的,是因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。
占用空间方面
在上面的虚函数实现原理部分,可以看到为了实现运行时多态机制,编译器会给每一个包含虚函数或继承了虚函数的类自动建立一个虚函数表,所以虚函数的一个代价就是会增加类的体积。在虚函数接口较少的类中这个代价并不明显,虚函数表vtbl的体积相当于几个函数指针的体积,如果你有大量的类或者在每个类中有大量的虚函数,你会发现 vtbl 会占用大量的地址空间。但这并不是最主要的代价,主要的代价是发生在类的继承过程中,在上面的分析中,可以看到,当子类继承父类的虚函数时,子类会有自己的vtbl,如果子类只覆盖父类的一两个虚函数接口,子类vtbl的其余部分内容会与父类重复。这在如果存在大量的子类继承,且重写父类的虚函数接口只占总数的一小部分的情况下,会造成大量地址空间浪费。在一些GUI库上这种大量子类继承自同一父类且只覆盖其中一两个虚函数的情况是经常有的,这样就导致UI库的占用内存明显变大。 由于虚函数指针vptr的存在,虚函数也会增加该类的每个对象的体积。在单继承或没有继承的情况下,类的每个对象会多一个vptr指针的体积,也就是4个字节;在多继承的情况下,类的每个对象会多N个(N=包含虚函数的父类个数)vptr的体积,也就是4N个字节。当一个类的对象体积较大时,这个代价不是很明显,但当一个类的对象很轻量的时候,如成员变量只有4个字节,那么再加上4(或4N)个字节的vptr,对象的体积相当于翻了1(或N)倍,这个代价是非常大的。
C++虚函数浅析
3.纯虚函数,为什么需要纯虚函数?
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”

virtual void funtion1()=0

原因:
1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。

定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

虚函数和纯虚函数的区别
4.为什么需要虚析构函数,什么时候不需要?父类的析构函数为什么要定义为虚函数
一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。
当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。
5.内联函数、构造函数、静态成员函数可以是虚函数吗?
inline, static, constructor三种函数都不能带有virtual关键字。
inline是编译时展开,必须有实体;
static属于class自己的,也必须有实体;
static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。
静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。

虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.

对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.

虚函数的调用关系:this -> vptr -> vtable ->virtual function

virtual函数基于vtable(内存空间),constructor函数如果是virtual的,调用时也需要根据vtable寻找,但是constructor是virtual的情况下是找不到的,因为constructor自己本身都不存在了,创建不到class的实例,没有实例,class的成员(除了public static/protected static for friend class/functions,其余无论是否virtual)都不能被访问了。

虚函数实际上不能被内联:虚函数运行时所需的代价主要是虚函数不能是内联函。这也是非常好理解的,是因为内联函数是指在编译期间用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。

构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。

静态的对象是属于整个类的,不对某一个对象而言,同时其函数的指针存放也不同于一般的成员函数,其无法成为一个对象的虚函数的指针以实现由此带来的动态机制。
构造函数中可以调用虚函数吗?
最后,总结一下关于虚函数的一些常见问题:

  1. 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数。这是虚函数的基本功能,就不再解释了。

  2. 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。

  3. 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。

  4. 将一个函数定义为纯虚函数,实际上是将这个类定义为抽象类,不能实例化对象。

  5. 纯虚函数通常没有定义体,但也完全可以拥有。

  6. 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。

  7. 非纯的虚函数必须有定义体,不然是一个错误。

  8. 派生类的override虚函数定义必须和父类完全一致。除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。例如,在上面的例子中,在Base中定义了 virtual Base* clone(); 在Derived中可以定义为 virtual Derived* clone()。可以看到,这种放松对于Clone模式是非常有用的。
    虚析构函数(√)、纯虚析构函数(√)、虚构造函数(X)
    6.为什么需要虚继承?虚继承实现原理解析,
    虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。
    如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类,虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示,

1、声明(declaration )指定了一个变量的标识符,用来描述变量的类型,是类型还是对象,或者函数等。声明,用于编译器(compiler)识别变量名所引用的实体。以下这些就是声明:

extern int bar;

extern int g(int, int);

double f(int, double); // 对于函数声明,extern关键字是可以省略的。

class foo; // 类的声明,前面是不能加class的。

2、

定义是对声明的实现或者实例化。连接器(linker)需要它(定义)来引用内存实体。与上面的声明相应的定义如下:

int bar;

int g(int lhs, int rhs) {return lhs*rhs;}

double f(int i, double d) {return i+d;}

class foo {};// foo 这里已经拥有自己的内存了,对照上面两个函数,你就应该明白{}的用处了吧?

无论如何,定义 操作是只能做一次的。如果你忘了定义一些你已经声明过的变量,或者在某些地方被引用到的变量,那么,连接器linker是不知道这些引用该连接到那块内存上的。然后就会报missing symbols 这样的错误。如果你定义变量超过一次,连接器是不知道把引用和哪块内存连接,然后就会报 duplicated symbols这样的错误了。以上的symbols其实就是指定义后的变量名,也就是其标识的内存块

编译

​​动态编译与静态编译

静态编译,编译器在编译可执行文件时,把需要用到的对应动态链接库中的部分提取出来,连接到可执行文件中去,使可执行文件在运行时不需要依赖于动态链接库
动态编译的可执行文件需要附带一个动态链接库在执行时,需要调用其对应动态链接库的命令
所以其优点一方面是缩小了执行文件本身的体积,另一方面是加快了编译速度,节省了系统资源
缺点是哪怕是很简单的程序,只用到了链接库的一两条命令,也需要附带一个相对庞大的链接库;二是如果其他计算机上没有安装对应的运行库,则用动态编译的可执行文件就不能运行。

动态联编与静态联编

在C++中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可以分为静态联编和动态联编;
静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差
动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果

​​​​​​​C语言的编译链接过程?

源代码-->预处理-->编译-->优化-->汇编-->链接–>可执行文件

预处理
读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件.i预处理后的c文件,.ii预处理后的C++文件。

编译阶段
编译程序所要作得工作就是通过词法分析语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码.s文件

汇编过程
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。.o目标文件

链接阶段
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

​​​​​​​hello world 程序开始到打印到屏幕上的全过程?

1.用户告诉操作系统执行HelloWorld程序(通过键盘输入等)

2.操作系统:找到helloworld程序的相关信息,检查其类型是否是可执行文件;并通过程序首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址

3.操作系统:创建一个新进程,将HelloWorld可执行文件映射到该进程结构,表示由该进程执行helloworld程序。

4.操作系统:为helloworld程序设置cpu上下文环境,并跳到程序开始处

5.执行helloworld程序的第一条指令,发生缺页异常

6.操作系统:分配一页物理内存,并将代码从磁盘读入内存,然后继续执行helloworld程序

7.helloword程序执行puts函数(系统调用),在显示器上写一字符串

8.操作系统:找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程

9.操作系统:控制设备的进程告诉设备的窗口系统,它要显示该字符串,窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储映像区

10.视频硬件将像素转换成显示器可接收的一组控制数据信号

11.显示器解释信号激发液晶屏

12.OK,我们在屏幕上看到了HelloWorld

类型

内置类型

算数类型和空类型

(1)16位平台
char 1个字节8位
short 2个字节16位
int 2个字节16位
long 4个字节32位
指针 2个字节16位
(2)32位平台
char 1个字节8位
short 2个字节16位
int 4个字节32位
long 4个字节32位
long long 8个字节64位
指针 4个字节32位
(3)64位平台
char 1个字节
short 2个字节
int 4个字节
long 8个字节(区别)
long long 8个字节
指针 8个字节(区别)

“零值比较”?

bool类型:if(flag)

int类型:if(flag == 0)

指针类型:if(flag == null)

float类型:if((flag >= -0.000001) && (flag <= 0. 000001))

引用

引用的实现原理
int a=1;
int &b=a

1.引用变量b和被引用变量a并没有共用一块内存,b是另外开辟了一块内存的
2.引用变量b开辟的内存中存放的是a的地址
3.任何对变量b的操作,都将转换为对(*b)的操作,比如b=b+1实际上是(*b)=(*b)+1 而(*b)代表的就是a
4.基于上面3点我们可以总结出 引用变量b可以理解为被引用变量a的别名

再看一个实际的例子

#include<iostream>
using namespace std;int main(){
    int  a = 1;
    int&  b = a;
    cout << "a:address->" << &a << endl;
    cout << "b:address->" << &b << endl;

    getchar();
    return 0;
}

运行结果:

a:address->0031FD54 
b:address->0031FD54

1.引用必须在声明引用时将其初始化,而不能先声明,再赋值。也不能在使用过程中途对其赋值企图更改被引用的值,那样是无效的
比如:

int rats = 101;
int & rodents = rats; 
int bunnies = 50;
rodents = bunnies;  

在上面一通操作以后rodent引用的还是rats

2.在用引用作为函数形参的时候,如果实参与引用参数不匹配,C++将生成临时参数。使用临时参数的数据改变不会影响传入的数据
比如:

void swap(int &a,int &b)
{
    int temp;
 
    temp=a;
    a=b;
    b=temp;
}
 
long a=3,b=5;
swap(a,b);

这里的a,b与传入函数参数的a,b类型不匹配,因此编译器将创建两个临时int变量,将它们初始为3和5,然后交换临时变量的内容,而a和b保持不变。

左值引用和右值引用

1)左值和右值的区分:
能否对表达式取地址
具名变量或对象都是左值,而匿名变量则是右值

int a = 5;
int &b = a; // b是左值引用
b = 4;
int &c = 10; // error,10无法取地址,无法进行引用
const int &d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址
int a = 4;int &&b = a; // error, a是左值
int &&c = std::move(a); // ok

2)左值引用和右值引用的区别:
对左/右值进行绑定的引用

3)纯右值和将亡值:
纯右值:在表达式结束之后就销毁了
非引用返回的临时变量
运算表达式产生的临时变量
原始字面量
lambda表达式

将亡值:与右值引用相关的表达式,生命周期将会通过右值引用得以延续
将要被移动的对象
T&&函数返回值
std::move返回值
转换为T&&的类型的转换函数的返回值等

4)右值引用的特点和作用:

a)生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去
可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构
常量左值引用是一个“万能”的引用类型,可以接受左值、右值、常量左值和常量右值。需要注意的是普通的左值引用不能接受右值。

b)右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值
引用折叠:
所有的右值引用叠加到右值引用上仍然还是一个右值引用;
所有的其他引用类型之间的叠加都将变成左值引用。

c)仅仅是当发生自动类型推导(如函数模板的类型自动推导,或auto关键字)的时候,T&&才是universal references
T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化

右值引用的作用:
通过移动语义来避免无谓拷贝的问题
通过move语义来将临时生成的左值中的资源无代价的转移到另外一个对象中去
通过完美转发来解决不能按照参数实际类型来转发的问题(同时,完美转发获得的一个好处是可以实现移动语义)

5)移动语义

std::vector<string> vecs;...
std::vector<string> vecm = std::move(vecs); // 免去很多拷贝
A(A&& a) : m_val(val){ a.m_val=nullptr; }

移动构造函数的参数是一个确定的右值引用类型,只能接受右值参数,而A a = Get(false);函数返回值是右值,所以就会匹配到这个构造函数。这里的A&&可以看作是临时值的标识对于临时值我们仅仅需要做浅拷贝即可,无需再做深拷贝,从而解决了临时变量拷贝构造产生的性能损失的问题。

class A {
public:    
	A(int size) : size_(size) {        
	data_ = new int[size];    
	}    
	A(){}    
	A(const A& a) {        
	size_ = a.size_;        
	data_ = new int[size_];        
	cout << "copy " << endl;    
	}    
	A(A&& a) {        
	this->data_ = a.data_;        
	a.data_ = nullptr;        
	cout << "move " << endl;    
	}    
	~A() {        
	if (data_ != nullptr) {         
	delete[] data_;        
		}
	}    
	int *data_;    
	int size_;
};
int main() {    
	A a(10);    
	A b = a;    
	A c = std::move(a); // 调用移动构造函数    
	return 0;
}

解决的问题:
一个带有堆内存的类,必须提供一个深拷贝拷贝构造函数,因为默认的拷贝构造函数是浅拷贝,会发生“指针悬挂”的问题

class A {
public:    
	A(int size) : size_(size) {        
	data_ = new int[size];    
	}    
	A(){}    
	A(const A& a) { //浅拷贝       
	size_ = a.size_;        
	data_ = a.data_;        
	cout << "copy " << endl;    
	} 
	A(const A& a) {//深拷贝        
	size_ = a.size_;        
	data_ = new int[size_];        
	cout << "copy " << endl;    
	}   
	~A() {        
	delete[] data_;    
	}    
	int *data_;    
	int size_;
};
int main() {    
	A a(10);    
	A b = a;    
	cout << "b " << b.data_ << endl;    
	cout << "a " << a.data_ << endl;    
	return 0;
}

使用时注意:
a.提供移动构造函数的同时也会提供一个拷贝构造函数,以防止移动不成功的时候还能拷贝构造,使我们的代码更安全
b.移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数

6)move语义:
移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借助移动语义来优化性能呢,那该怎么做呢?
C++11为了解决这个问题,提供了std::move方法来将左值转换为右值,从而方便应用移动语义。move是将对象资源的所有权从一个对象转移到另一个对象,只是转移,没有内存的拷贝,这就是所谓的move语义
实际上将左值变成右值引用,然后应用移动语义,调用移动构造函数,就避免了拷贝,提高了程序性能

7)完美转发:
C++11引入了完美转发:在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数。C++11中的std::forward正是做这个事情的,他会按照参数的实际类型进行转发

右值引用T&&是一个universal references,可以接受左值或者右值,正是这个特性让他适合作为一个参数的路由,然后再通过std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发

void PrintV(int &t){cout << "lvalue" << endl;}
void PrintV(int &&t){cout << "rvalue" << endl;}
template<typename T>
void Test(T &&t) {    
	PrintV(t);    
	PrintV(std::forward<T>(t));
    PrintV(std::move(t));
}
int main() {    
	Test(1); // lvalue rvalue rvalue    
	int a = 1;    
	Test(a); // lvalue lvalue rvalue    
	Test(std::forward<int>(a));//lvalue rvalue rvalue
	Test(std::forward<int&>(a));//lvalue lvalue rvalue
	Test(std::forward<int&&>(a));//lvalue rvalue rvalue    
	return 0;
}

8)返回值优化
产生原因:
返回值优化(RVO)是一种C++编译优化技术当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。

这是因为在实际工程中大多数时候C++构造对象的开销巨大,编译器为了生成高效的代码,在foo函数返回时并没有调用拷贝构造函数去生成一个临时对象,而是直接使用在foo函数内初始化的对象c1作为返回值传出去。这就是C++中的返回值优化(Return Value Optimization, RVO)。

那什么时候编译器会进行返回值优化呢?
return的值类型与函数的返回值类型相同
return的是一个局部对象

看几个例子:
示例1:

std::vector<int> return_vector(void) {    
	std::vector<int> tmp {1,2,3,4,5};    
	return tmp;
}
std::vector<int> &&rval_ref = return_vector();

不会触发RVO,拷贝构造了一个临时的对象,临时对象的生命周期和rval_ref绑定,等价于下面这段代码:

const std::vector<int>& rval_ref = return_vector();

示例2:

std::vector<int>&& return_vector(void) {    
std::vector<int> tmp {1,2,3,4,5};    
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();

这段代码会造成运行时错误,因为rval_ref引用了被析构的tmp。

示例3:

std::vector<int> return_vector(void) {    
std::vector<int> tmp {1,2,3,4,5};    
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();

和示例1类似,std::move一个临时对象是没有必要的,也会忽略掉返回值优化。
最好的代码:

std::vector<int> return_vector(void) {    
std::vector<int> tmp {1,2,3,4,5};    
return tmp;
}
std::vector<int> rval_ref = return_vector();

这段代码会触发RVO,不拷贝也不移动,不生成临时对象

缺点:
a.返回值优化能在一定程度上提高程序的运行效率,但是我们在实际工程中最好不要依赖这个编译器优化特性,因为它要求的条件非常苛刻

b.返回值优化还带来了一个问题,那就是拷贝构造函数和析构函数的调用变得不可预测。如果在拷贝构造函数和析构函数中存在有副作用(side-effect)的语句,就会造成不同的编译器配置编译出来的程序运行结果不一致。事实上,这是被C++11标准所允许的,

是否调用拷贝构造函数和析构函数的决定交给了编译器实现去判断,所以在实际工程中为了写出可移植的代码,就需要避免在构造函数和析构函数中加入有副作用的语句,并且应该尽量把复杂的逻辑剥离出来,放在类的其它成员函数中实现。

指针和引用

指针、引用&、解引用*、取地址&的概念

概念:
指针指向一块内存,指针保存的是内存的地址;引用是变量的别名,本质是引用该变量的地址。
解引用是取指针指向的地址的内容,取地址是获得变量在内存中的地址。
区别:
(1)引用使用是无需解引用,指针需解引用。
(2)引用不能为空,指针可以为空。
(3)引用在定义时被初始化一次,之后不可变;指针指向的值和本身的值是可变的,也就是说指针只是一块地址,地址里的东西可变。

自定义类型

struct
struct和class的区别,struct与union的区别

struct和class都是声明类的关键字
区别是:
(1)在默认情况下,struct的成员变量是公共(public)的;在默认情况下,class的成员变量是私有(private)的。
(2)struct保证成员按照声明顺序在内存中存储。class不能保证。
(3)对于继承来说,class默认是private继承,struct默认是public继承。
区别是(union和class同理):
(1)一个union类型的变量,所有成员变量共享一块内存,该内存的大小有这些成员变量中长度最大的一个来决定,struct中成员变量内存都是独立的。
因此,对于union的一个成员赋值, 那么其它成员会重写,而struct则不会
(2)union分配的内存是连续的,而struct不能保证分配的内存是连续的

struct的内存对齐

默认的对齐方式:各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。
同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节

注:VC对变量存储的一个特殊处理。为了提高CPU的存储速度,VC对一些变量的起始地址做了“对齐”处理。在默认情况下,VC规定:
各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。

(1)示例代码一:

struct MyStruct{
     double dda1;
     char dda;
     int type;
};
//错:sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13。
//对:当在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为16。

注:为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式。

(1)先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(double)的倍数),该成员变量占用sizeof(double)=8个字节;

(2)接下来为第二个成员dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把dda存放在偏移量为8的地方满足对齐方式,
该成员变量占用sizeof(char)=1个字节;

(3)接下来为第三个成员type分配空间,
这时下一个可以分配的地址对于结构的起始地址的偏移量为9,
不是sizeof(int)=4的倍数,为了满足对齐方式对偏移量的约束问题,
VC自动填充3个字节(这三个字节没有放什么东西),
这时下一个可以分配的地址对于结构的起始地址的偏移量为12,
刚好是sizeof(int)=4的倍数,所以把type存放在偏移量为12的地方,
该成员变量占用sizeof(int)=4个字节;

这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,
所以没有空缺的字节需要填充。
所以整个结构的大小为:sizeof(MyStruct)=8+1+3+4=16,
其中有3个字节是VC自动填充的,没有放任何有意义的东西。

(2)示例代码二:交换一下上述例子中MyStruct的成员变量的位置

struct MyStruct{
     char dda;
     double dda1;
     int type;
};
//错:sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13。
//对:当在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为24。

注:为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式。

(1)先为第一个成员dda分配空间,
其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(char)的倍数),
该成员变量占用sizeof(char)=1个字节;

(2)接下来为第二个成员dda1分配空间,
这时下一个可以分配的地址对于结构的起始地址的偏移量为1,
不是sizeof(double)=8的倍数,需要补足7个字节才能使偏移量变为8(满足对齐方式),
因此VC自动填充7个字节,dda1存放在偏移量为8的地址上,它占用8个字节;

(3)接下来为第三个成员type分配空间,
这时下一个可以分配的地址对于结构的起始地址的偏移量为16,
是sizeof(int)=4的倍数,满足int的对齐方式,所以不需要VC自动填充,
type存放在偏移量为16的地址上,该成员变量占用sizeof(int)=4个字节;
这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:1+7+8+4=20,
不是结构的节边界数
(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,
所以需要填充4个字节,以满足结构的大小为sizeof(double)=8的倍数。
所以该结构总的大小为:sizeof(MyStruct)为1+7+8+4+4=24。
其中总的有7+4=11个字节是VC自动填充的,没有放任何有意义的东西。

字节的对齐方式:

在VC中提供了#pragmapack(n)来设定变量以n字节对齐方式。
n字节对齐就是说变量存放的起始地址的偏移量有两种情况:
第一,如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式;
第二,如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。
结构的总大小也有个约束条件,分下面两种情况:
如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;
否则必须为n的倍数。

注:VC对结构的存储的特殊处理确实提高了CPU存储变量的速度,但有时也会带来一些麻烦,我们也可以屏蔽掉变量默认的对齐方式,自己来设定变量的对齐方式。

(1)示例代码:

#pragmapack(push)//保存对齐状态
  
  
#pragmapack(4)//设定为4字节对齐
  
struct test{
      char m1;
      double m4;
      int m3;
};
 
 #pragmapack(pop)//恢复对齐状态

注:以上结构的大小为16,下面分析其存储情况。

(1)首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节;

(2)接着开始为m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于n),m4占用8个字节;

(3)接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节;
这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。
如果把上面的#pragmapack(4)改为#pragma pack(8),
那么我们可以得到结构的大小为24。

​为什么要内存对齐?

1.平台移植型好
不是所有的硬件平台都能访问任意地址上的数据;某些硬件平台只能只在某些地址访问某些特定类型的数据,否则抛出硬件异常,及遇到未对齐的边界直接就不进行读取数据了。
2.cpu处理效率高
从上图可以看出,对应两种存储方式,若CPU的读取粒度为4字节
那么对于一个int 类型,若是按照内存对齐来存储,处理器只需要访存一次就可以读取完4个字节
若没有按照内存对其来读取,如上图所示,就需要访问内存两次才能读取出一个完整的int 类型变量
具体过程为,第一次拿出 4个字节,丢弃掉第一个字节,第二次拿出4个字节,丢弃最后的三个字节,然后拼凑出一个完整的 int 类型的数据。
其实结构体内存对齐是拿空间换取时间的做法。提高效率

大端字节序和小端字节序

什么是大端字节序和小端字节序?

例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于 大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。

我们常用的X86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

小端:主机字节序
大端:网络字节序

为什么要有大端小端字节序 ?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于 8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

代码判断大端小端字节序
#include<stdio.h>
union node{
	int num;
	char ch;
}

int main(){
	union node p;
	p.num=0x12345678;
	if(p.ch==0x78) //小端
	......
	或者:
	char *q=&num;
	if(*q==0x78)//小端
}

类型转换

static_cast

在C++语言中static_cast用于数据类型的强制转换,强制将一种数据类型转换为另一种数据类型。例如将整型数据转换为浮点型数据。
[例1]C语言所采用的类型转换方式:

int a = 10;
int b = 3;
double result = (double)a / (double)b;

例1中将整型变量a和b转换为双精度浮点型,然后相除。在C++语言中,我们可以采用static_cast关键字来进行强制类型转换,如下所示。
[例2]static_cast关键字的使用:

int a = 10;
int b = 3;
double result = static_cast<double>(a) / static_cast<double>(b);

在本例中同样是将整型变量a转换为双精度浮点型。采用static_cast进行强制数据类型转换时,将想要转换成的数据类型放到尖括号中,将待转换的变量或表达式放在元括号中,其格式可以概括为如下形式:

用法:static_cast <类型说明符> (变量或表达式)

它主要有如下几种用法:
(1)用于类层次结构中基类和派生类之间指针或引用的转换
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
进行下行转换(把基类的指针或引用转换为派生类表示),由于没有动态类型检查,所以是不安全的
(2)用于基本数据类型之间的转换,如把int转换成char。这种转换的安全也要开发人员来保证
(3)把空指针转换成目标类型的空指针
(4)把任何类型的表达式转换为void类型

注意:static_cast不能转换掉expression的const、volitale或者__unaligned属性。
static_cast:可以实现C++中内置基本数据类型之间的相互转换。

如果涉及到类的话,static_cast只能在有相互联系的类型中进行相互转换,不一定包含虚函数。

为什么要用static_cast转换而不用c语言中的转换?

更加安全;
更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;
可清楚地辨别代码中每个显式的强制转换;
可读性更好,能体现程序员的意图

const_cast

在C语言中,const限定符通常被用来限定变量,用于表示该变量的值不能被修改。

而const_cast则正是用于强制去掉这种不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。

用法:const_cast<type_id> (expression)
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;
常量引用被转换成非常量引用,并且仍然指向原来的对象;
常量对象被转换成非常量对象。

[例3]一个错误的例子:

const int a = 10;
const int * p = &a;
*p = 20;                  //compile error
int b = const_cast<int>(a);  //compile error

在本例中出现了两个编译错误,第一个编译错误是*p因为具有常量性,其值是不能被修改的;另一处错误是const_cast强制转换对象必须为指针或引用,而例3中为一个变量,这是不允许的!

[例4]const_cast关键字的使用

#include<iostream>
using namespace std;
 
int main(){
    const int a = 10;
    const int * p = &a;
    int *q;
    q = const_cast<int *>(p);
    *q = 20;    //fine
    cout <<a<<" "<<*p<<" "<<*q<<endl;
    cout <<&a<<" "<<p<<" "<<q<<endl;
    return 0;
}

在本例中,我们将变量a声明为常量变量,同时声明了一个const指针指向该变量(此时如果声明一个普通指针指向该常量变量的话是不允许的,Visual Studio 2010编译器会报错)。

之后我们定义了一个普通的指针*q。将p指针通过const_cast去掉其常量性,并赋给q指针。之后我再修改q指针所指地址的值时,这是不会有问题的。

最后将结果打印出来,运行结果如下:

10 20 20
002CFAF4 002CFAF4 002CFAF4

查看运行结果,问题来了,指针p和指针q都是指向a变量的,指向地址相同,而且经过调试发现002CFAF4地址内的值确实由10被修改成了20,这是怎么一回事呢?为什么a的值打印出来还是10呢?

其实这是一件好事,我们要庆幸a变量最终的值没有变成20!变量a一开始就被声明为一个常量变量,不管后面的程序怎么处理,它就是一个常量,就是不会变化的。试想一下如果这个变量a最终变成了20会有什么后果呢?对于这些简短的程序而言,如果最后a变成了20,我们会一眼看出是q指针修改了,但是一旦一个项目工程非常庞大的时候,在程序某个地方出现了一个q这样的指针,它可以修改常量a,这是一件很可怕的事情的,可以说是一个程序的漏洞,毕竟将变量a声明为常量就是不希望修改它,如果后面能修改,这就太恐怖了。

在例4中我们称“*q=20”语句为未定义行为语句,所谓的未定义行为是指在标准的C++规范中并没有明确规定这种语句的具体行为,该语句的具体行为由编译器来自行决定如何处理。对于这种未定义行为的语句我们应该尽量予以避免!

从例4中我们可以看出我们是不想修改变量a的值的,既然如此,定义一个const_cast关键字强制去掉指针的常量性到底有什么用呢?我们接着来看下面的例子。

例5:

#include<iostream>
using namespace std;
 
const int * Search(const int * a, int n, int val);
 
int main()
{
    int a[10] = {0,1,2,3,4,5,6,7,8,9};
    int val = 5;
    int *p;
    p = const_cast<int *>(Search(a, 10, val));
    if(p == NULL)
        cout<<"Not found the val in array a"<<endl;
    else
        cout<<"hvae found the val in array a and the val = "<<*p<<endl;
    return 0;
}
 
const int * Search(const int * a, int n, int val)
{
    int i;
    for(i=0; i<n; i++)
    {
        if(a[i] == val)
            return &a[i];
    }
    return  NULL;
}

在例5中我们定义了一个函数,用于在a数组中寻找val值,如果找到了就返回该值的地址,如果没有找到则返回NULL。函数Search返回值是const指针,当我们在a数组中找到了val值的时候,我们会返回val的地址,最关键的是a数组在main函数中并不是const,因此即使我们去掉返回值的常量性有可能会造成a数组被修改,但是这也依然是安全的。

对于引用,我们同样能使用const_cast来强制去掉常量性,如例6所示。

例6:

#include<iostream>
using namespace std;
 
const int & Search(const int * a, int n, int val);
 
int main(){
	int a[10] = {0,1,2,3,4,5,6,7,8,9};
	int val = 5;
	int &p = const_cast<int &>(Search(a, 10, val));
	if(p == NULL)
		cout<<"Not found the val in array a"<<endl;
	else
		cout<<"hvae found the val in array a and the val = "<<p<<endl;
	return 0;
}
 
const int & Search(const int * a, int n, int val)
{
	int i;
	for(i=0; i<n; i++){
		if(a[i] == val)
		return a[i];
	}
	return NULL;
}

了解了const_cast的使用场景后,可以知道使用const_cast通常是一种无奈之举,同时也建议大家在今后的C++程序设计过程中一定不要利用const_cast去掉指针或引用的常量性并且去修改原始变量的数值,这是一种非常不好的行为。

reinterpret_cast

在C++语言中,reinterpret_cast主要有三种强制转换用途:
改变指针或引用的类型;
将指针或引用转换为一个足够长度的整形;
将整型转换为指针或引用类型。

用法:reinterpret_cast<type_id> (expression)
type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
在使用reinterpret_cast强制转换过程仅仅只是比特位的拷贝,因此在使用过程中需要特别谨慎!

例7:

int *a = new int;
double *d = reinterpret_cast<double *>(a);

在例7中,将整型指针通过reinterpret_cast强制转换成了双精度浮点型指针。
reinterpret_cast可以将指针或引用转换为一个足够长度的整型,此中的足够长度具体长度需要多少则取决于操作系统,如果是32位的操作系统,就需要4个字节及以上的整型,如果是64位的操作系统则需要8个字节及以上的整型。

dynamic_cast

用法:dynamic_cast<type_id> (expression)

(1)其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时要进行类型检查

(2)不能用于内置的基本数据类型的强制转换

(3)dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL。

(4)使用dynamic_cast进行转换的,基类中一定要有虚函数,否则编译不通过。

    基类需要检测有虚函数的原因:
    类中存在虚函数,就说明它有想要让基类指针或引用指向派生类对象的情况,此时转换才有意义。

   
    这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见<Inside c++ object model>)中,
    只有定义了虚函数的类才有虚函数表。

(5)在类的转换时,在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的。在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

向上转换,即为子类指针指向父类指针(一般不会出问题);
向下转换,即将父类指针转化子类指针。
向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败。

在C++中,编译期的类型转换有可能会在运行时出现错误,特别是涉及到类对象的指针或引用操作时,更容易产生错误。Dynamic_cast操作符则可以在运行期对可能产生问题的类型转换进行测试

例1:

#include<iostream>using namespace std;
class base{
public :
    void m(){cout<<"m"<<endl;}
};
class derived : public base{
public:
    void f(){cout<<"f"<<endl;}
};
 int main(){
    derived * p;
    p = new base;
    p = static_cast<derived *>(new base);
    p->m();
    p->f();
    return 0;
}

本例中定义了两个类:base类和derived类,这两个类构成继承关系。在base类中定义了m函数,derived类中定义了f函数。在前面介绍多态时,我们一直是用基类指针指向派生类或基类对象,而本例则不同了。

本例主函数中定义的是一个派生类指针,当我们将其指向一个基类对象时,这是错误的,会导致编译错误

但是通过强制类型转换我们可以将派生类指针指向一个基类对象,
p = static_cast<derived *>(new base);语句实现的就是这样一个功能,这样的一种强制类型转换时合乎C++语法规定的,但是是非常不明智的,它会带来一定的危险。

在程序中p是一个派生类对象,我们将其强制指向一个基类对象,首先通过p指针调用m函数,因为基类中包含有m函数,这一句没有问题,之后通过p指针调用f函数。一般来讲,因为p指针是一个派生类类型的指针,而派生类中拥有f函数,因此p->f();这一语句不会有问题,但是本例中p指针指向的确实基类的对象,而基类中并没有声明f函数,虽然p->f();这一语句虽然仍没有语法错误,但是它却产生了一个运行时的错误。换言之,p指针是派生类指针,这表明程序设计人员可以通过p指针调用派生类的成员函数f,但是在实际的程序设计过程中却误将p指针指向了一个基类对象,这就导致了一个运行期错误。

产生这种运行期的错误原因在于static_cast强制类型转换时并不具有保证类型安全的功能,而C++提供的dynamic_cast却能解决这一问题,dynamic_cast可以在程序运行时检测类型转换是否类型安全。

当然dynamic_cast使用起来也是有条件的,它要求所转换的操作数必须包含多态类类型(即至少包含一个虚函数的类)。

例2:

#include<iostream>
using namespace std;
class base{
public :
    void m(){cout<<"m"<<endl;}
};
class derived : public base{
public:
    void f(){cout<<"f"<<endl;}
};
int main(){
    derived * p;
    p = new base;
    p = dynamic_cast<derived *>(new base);
    p->m();
    p->f();
    return 0;
}

在本例中利用dynamic_cast进行强制类型转换,但是因为base类中并不存在虚函数,因此p = dynamic_cast<derived *>(new base);这一句会编译错误。

为了解决本例中的语法错误,我们可以将base类中的函数m声明为虚函数,virtual void m(){cout<<“m”<<endl;}。
dynamic_cast还要求<>内部所描述的目标类型必须为指针或引用

例3:

#include<iostream>
#include<cstring>
using namespace std;
class A{
   public:
   virtual void f(){
       cout<<"hello"<<endl;
   };
};
 
class B:public A{
    public:
    void f(){
        cout<<"hello2"<<endl;
    };
};
 
  
class C{
  void pp(){
      return;
  }
};
 
int fun(){
    return 1;
}
 int main(){
    A* a1=new B;//a1是A类型的指针指向一个B类型的对象
    A* a2=new A;//a2是A类型的指针指向一个A类型的对象
    B* b;
    C* c;
    b=dynamic_cast<B*>(a1);//结果为not null,向下转换成功,a1之前指向的就是B类型的对象,所以可以转换成B类型的指针。
    if(b==NULL){
        cout<<"null"<<endl;
    }
 
    else{
        cout<<"not null"<<endl;
    }
 
    b=dynamic_cast<B*>(a2);//结果为null,向下转换失败
    if(b==NULL){
        cout<<"null"<<endl;
    }
 
    else{
        cout<<"not null"<<endl;
    }
 
    c=dynamic_cast<C*>(a);//结果为null,向下转换失败
    if(c==NULL){
        cout<<"null"<<endl;
    }
 
    else{
        cout<<"not null"<<endl;
    }
 
    delete(a);
    return 0;
}

动态内存管理

内存结构

在这里插入图片描述
1.从静态存储区域分配。该存储区域在程序编译的时候就已经分配好了,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2. 在栈上创建。在执行函数时,函数的局部变量存储在该区域,函数执行结束时会释放该存储空间。栈内存分配运算内置于处理器的指令集
3. 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。 动态内存的生存期由程序员决定,使用非常灵活。

堆栈相关问题

堆和栈的区别

(1)一个是静态的,一个是动态的,堆是静态的,由用户申请和释放,栈是动态的,保存程序的局部变量
(2)申请后系统的响应不同
栈:只要栈的剩余空间大于申请空间,系统就为程序提供内存,否则将抛出栈溢出异常
堆:当系统收到程序申请时,先遍历操作系统中记录空闲内存地址的链表,寻找第一个大于所申请空间的堆结点,然后将该结点从空间结点链表中删除,并将该结点的空间分配给程序。
(3)申请大小限制的不同
栈:在windows下,栈的大小一般是2M,如果申请的空间超过栈的剩余空间时,将提示overflow。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大

1、管理方式不同;
对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

2、空间大小不同;
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的

3、碎片问题不同;
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出

4、生长方向不同;
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

5、分配方式不同;
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

6、分配效率不同;
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,

堆栈溢出的原因?

没有回收垃圾资源
栈溢出:
一般都是由越界访问导致的。例如局部变量数组越界访问或者函数内局部变量使用过多,超出了操作系统为该进程分配的栈的大小。
堆溢出:
由于堆是用户申请的,所以溢出的原因可能是程序员申请了资源但是忘记释放了。

什么是内存泄露?C++内存泄漏检测

内存泄露是指程序中动态分配了内存,但是在程序结束时没有释放这部分内存,从而造成那一部分内存不可用的情况
有一些内存泄漏的检测工具,比如 BoundsChecker
静态内存泄漏通过工具或者仔细检查代码找到泄漏点。
动态的内存泄漏很难查,一般通过在代码中加断点跟踪Run-Time内存检测工具来查找。
内存泄漏的检测可以分以下几个步骤:
(1)看代码new之后是否delete,就是申请了静态内存用完是否释放。看析构函数是否真的执行,如果没有真正执行,就需要动态释放对象;
(2)让程序长时间运行,看任务管理器对应程序内存是不是一直向上增加;
(3)使用常用内存泄漏检测工具来检测内存泄漏点。

第一:良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉。

第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。

第三:Boost 中的smart pointer。

第四:一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等等。

malloc和free实现原理

从操作系统角度看,进程分配内存有两种方式,分别由两个系统调用完成:brk 和 mmap (不考虑共享内存)

1)brk 是将数据段(.data)的最高地址指针 _edata 往高地址推
2)mmap 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系

malloc和free过程

情况一:malloc 小于 128K 的内存,使用 brk 分配
  malloc函数会调用brk系统调用,将_edata指针往高地址推,等到进程第一次读写这块内存的时候,发生缺页中断,这个时候,内核才分配这块内存对应的物理页。也就是说,如果用malloc分配了这块内容,然后从来不访问它,那么,A对应的物理页是不会被分配的。

情况二:malloc 大于 128K 的内存,使用 mmap 分配(munmap 释放)

默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存

这样做主要是因为:

brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,因为只有一个_edata 指针,这就是内存碎片产生的原因,什么时候紧缩看下面),而mmap分配的内存可以单独释放

情况三 使用free
  当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。

new、delete;malloc、free关系

new和delete是一组,new用调用构造函数来实例化对象和调用析构函数释放对象申请的资源。
malloc和free是一对,用来申请内存和释放内存,但是申请和释放的对象只能是内部数据类型。
区别:
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符
malloc/free只能操作内部数据类型

在这里插入图片描述

delete与 delete []区别

都是用来调用析构函数的:
(1)delete只会调用一次析构函数,delete[]会调用每一个成员的析构函数。
(2)delete与new配套,delete []与new []配套,用new分配的内存用delete删除用new[]分配的内存用delete[]删除

变量的分类

全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?static全局变量与普通的全局变量有什么区别?static局部变量和普通局部变量有什么区别?static函数与普通函数有什么区别?
1)变量可以分为:全局变量、局部变量、静态全局变量、静态局部变量
全局变量在整个工程文件内都有效;
静态全局变量只在定义它的文件内有效;
局部变量在定义它的函数内有效,这个函数返回会后失效。
静态局部变量只在定义它的函数内有效,只是程序仅分配一次内存,函数返回后,该变量不会消失,直到程序运行结束后才释放;
全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。局部变量的值不可知。

静态全局变量是定义存储类型为静态型的外部变量,其作用域是从定义点到程序结束,所不同的是存储类型决定了存储地点,静态型变量是存放在内存的数据区中的,它们在程序开始运行前就分配了固定的字节,在程序运行过程中被分配的字节大小是不改变的.只有程序运行结束后,才释放所占用的内存.

变量的作用域:
形参变量只在被调用期间才分配内存单元,调用结束立即释放。 这一点表明形参变量只有在函数内才是有效的, 离开该函数就不能再使用了。局部变量也称为内部变量。其作用域仅限于函数内, 离开该函数后再使用这种变量是非法的。
全局变量也称为外部变量,它不属于哪一个函数,它属于一个源程序文件。其作用域是整个源程序。在函数中使用全局变量,一般应作全局变量说明。 只有在函数内经过说明的全局变量才能使用。全局变量的说明符为extern。 但在一个函数之前定义的全局变量,在该函数内使用可不再加以说明。 对于全局变量还有以下几点说明:
外部变量可加强函数模块之间的数据联系, 但是又使函数要依赖这些变量,因而使得函数的独立性降低。从模块化程序设计的观点来看这是不利的, 因此在不必要时尽量不要使用全局变量。
在同一源文件中,允许全局变量和局部变量同名。在局部变量的作用域内,全局变量不起作用。

变量的存储方式可分为“静态存储”和“动态存储”两种。
静态存储变量通常是在变量定义时就分定存储单元并一直保持不变, 直至整个程序结束。动态存储变量是在程序执行过程中,使用它时才分配存储单元, 使用完毕立即释放。 如果一个函数被多次调用,则反复地分配、 释放形参变量的存储单元。从以上分析可知, 静态存储变量是一直存在的, 而动态存储变量则时而存在时而消失。我们又把这种由于变量存储方式不同而产生的特性称变量的生存期。 生存期表示了变量存在的时间。 生存期和作用域是从时间和空间这两个不同的角度来描述变量的特性,这两者既有联系,又有区别。 一个变量究竟属于哪一种存储方式, 并不能仅从其作用域来判断,还应有明确的存储类型说明。

从作用域看:

全局变量具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。
静态局部变量具有局部作用域,它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
局部变量也只有局部作用域,它是自动对象(auto),它在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
静态全局变量也具有全局作用域,它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。

从分配内存空间看:
全局变量,静态局部变量,静态全局变量都在静态存储区分配空间,而局部变量在栈里分配空间。
全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。
1)、静态变量会被放在程序的静态数据存储区(全局可见)中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
2)、变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。
程序的局部变量存在于(堆栈)中,全局变量存在于(静态区 )中,动态申请数据存在于( 堆)中。

第一:良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉。

第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。

第三:Boost 中的smart pointer。

第四:一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等等。

自动内存管理

C++98时代的智能指针auto_ptr

(1)C++98里面有一个智能指针auto_ptr,对于拷贝构造和赋值运算符重载,该智能指针采用管理权转移的方式(当一个指针拷贝构造另一个指针时,当前指针就将对空间的管理权交给拷贝的那个指针,当前指针就指向空);
(2)但是这种方式不符合指针的要求(可以允许多个指针指向同一块空间,将一个指针赋值给另一个指针的时候,就是需要让两个指针指向同一块空间,而auto_ptr只允许一块空间上只能有一个指针指向它),并且当管理权转移之后要想再访问之前的指针,就会出错,因为之前的指针已经为NULL,就会出现解引用空指针的问题。

Boost库中的智能指针
scope_ptr

scoped_ptr采用防拷贝的方式(防拷贝就是不允许拷贝,拷贝就会出错;防拷贝的实现:将拷贝构造和的赋值运算符重载只声明不实现,并且声明为私有);

shared_ptr

shared_ptr为共享指针,里面采用引用计数,当有shared_ptr指向同一块空间的时候就增加引用计数,当引用计数减为0的时候才释放该智能指针管理的那块空间。
但是shared_ptr有一个缺点,就是会出现循环引用的问题(当一个shared_ptr(如sp1)管理的空间里面包含一个shared_ptr的指针(_next),另一个shared_ptr(如sp2)管理的空间里面也包含一个shared_ptr指针(_prev)时,当sp1->_next = sp2;sp2->_prev = sp1;此时就会使得sp1和sp2的引用计数都变为2,当出了这个作用域sp1和sp2的引用计数都会减为1,但是只有引用计数为0时才会释放管理的空间,就会使得sp1和sp2管理的空间没有释放。

boost库里面还包含scoped_array和shared_array(这个适用于delete[]的场景)

C++11时代的智能指针

在这里插入图片描述
在这里插入图片描述

unique_ptr

unique_ptr是auto_ptr的继承者,对于同一块内存只能有一个持有者,而unique_ptr和auto_ptr唯一区别就是unique_ptr不允许赋值操作,也就是不能放在等号的右边(函数的参数和返回值例外),这一定程度避免了一些误操作导致指针所有权转移,然而,unique_str依然有提供所有权转移的方法move,调用move后,原unique_ptr就会失效,再用其访问裸指针也会发生和auto_ptr相似的crash,如下面示例代码,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指针所有权被转移。

shared_ptr

auto_ptr和unique_ptr都有或多或少的缺陷,因此C++11还推出了shared_ptr,这也是目前工程内使用最多最广泛的智能指针,他使用引用计数,实现对同一块内存可以有多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和unique_ptr最大的区别。

如下面代码所示,A中引用B,B中引用A,spa和spb的强引用计数永远大于等于1,所以直到程序退出前都不会被退出,这种情况有时候在正常的业务逻辑中是不可避免的,而解决循环引用的方法最有效就是改用weak_ptr

weak_ptr

weak_ptr来解决循环引用的问题,weak_ptr叫弱指针

它主要是为了配合shared_ptr使用,用来解决循环引用的问题;
即A内部有指向B,B内部有指向A,B必定是在A析构后B才析构,对于B,A必定是在B析构后才析构A,这就是循环引用问题,违反常规,导致内存泄露。

将会出现循环引用问题的指针用weak_ptr保存着,weak_ptr并不拥有这块空间,所以weak_ptr里面不增加shared_ptr的引用计数 也就不会掌控这这块空间的生命周期。(注意weak_ptr里面也有自己的引用计数

class A {
public:
    shared_ptr<B> b;
};
class B {
public:
    shared_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    shared_ptr<A> spa = make_shared<A>();
    shared_ptr<B> spb = make_shared<B>();
    spa->b = spb;
    spb->a = spa;
    return 0;
} 

可改造为如下代码。

class A {
public:
    shared_ptr<B> b;
};
class B {
public:
    weak_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    shared_ptr<A> spa = make_shared<A>();
    shared_ptr<B> spb = make_shared<B>();
    spa->b = spb; //spb强引用计数为2,弱引用计数为1
    spb->a = spa; //spa强引用计数为1,弱引用计数为2
    return 0;
} //main函数退出后,spa先释放,spb再释放,循环解开了

使用weak_ptr也有需要注意的点,因为既然weak_ptr不负责裸指针的生命周期,那么weak_ptr也无法直接操作裸指针,我们需要先转化为shared_ptr,,具体操作如下:

shared_ptr<int> spa = make_shared<int>(10);
weak_ptr<int> spb = spa; //weak_ptr无法直接使用裸指针创建
if (!spb.expired()) { //weak_ptr最好判断是否过期,使用expired或use_count方法,前者更快
    *spb.lock() += 10; //调用weak_ptr转化为shared_ptr后再操作裸指针
}
cout << *spa << endl; //20
智能指针的多线程安全问题

在这里插入图片描述

智能指针底层原理

在这里插入图片描述

5:如何引用一个已经定义过的全局变量?区别是什么
如果在同一个文件中,直接引用即可。
如果不在同一个文件,有两种方式:
(1)直接引用头文件就可以使用了。
(2)用extern关键字重新声明一下。

6:全局变量可不可以定义在可被多个.C文件包含的头文件中?
因为全局变量的作用域是整个源程序,可以声明多次,但是只能定义一次。变量的声明一般放在头文件中,那么变量的定义可以放在头文件中吗?在实际的编程中一般很少在头文件中定义全局变量,因为多次引用可能重定义。

9:main 函数执行以前,会执行什么代码?
全局对象的构造函数会在main 函数之前执行,比如int a;初始化为0。

10:main 主函数执行完毕后,会执行什么代码?
可以,使用on_exit 注册的函数会在代码执行完毕后执行:
void main( void )
{
String str(“zhanglin”);
on_exit( fn1 );
on_exit( fn2 );
on_exit( fn3 );
on_exit( fn4 );
printf( “This is executed first.\n” );
}
int fn1()
{
printf( “next.\n” );
return 0;
}

11:局部变量能否和全局变量重名?
可以,但是局部会屏蔽全局。要用全局变量,需要使用域作用符“::”。

13:类的成员函数重载、覆盖和隐藏的概念和区别?
概念:
重载是指再同一个作用域内,有几个同名的函数,但是参数列表的个数和类型不同。
函数覆盖是指派生类函数覆盖基类函数,函数名、参数类型、返回值类型一模一样。派生类的对象会调用子类中的覆盖版本,覆盖父类中的函数版本。
隐藏是指派生类的函数屏蔽了与其同名的基类函数。
覆盖和重载的区别:函数是否处在不同的作用域,参数列表是否一样;基类函数是否有virtual关键字。
隐藏和覆盖的区别:
(1)派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。
(2)派生类的函数与基类的函数同名,参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏有virtual,就是覆盖。
如果子类覆盖父类的函数但是不加virtual ,也能实现多态,由于virtual修饰符会被隐形继承,但是尽量加上。

15:const与#define的概念和优缺点?
const用来定义常量、修饰函数参数、修饰函数返回值,可以避免被修改,提高程序的健壮性。
define是宏定义,在编译的时候会进行替换,这样做的话可以避免没有意义的数字或字符串,便于程序的阅读。
区别:
1.const定义的数据有数据类型,而宏常量没有数据类型。编译器可以对const常量进行类型检查。而对宏定义只进行字符替换,没有类型安全检查,所以字符替换时可能出错。
例子:

写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。
#define MIN(A,B) ((A) <= (B) (A) : (B))
least = MIN(a, b);
下面的关键字const是什么含意:
const int a;//a是一个常整型数
int const a;//a是一个常整型数
const int *a;//a是一个指向常整型数的指针
int * const a;//a是一个指向整型数的常指针
int const * a const;//a是一个指向常整型数的常指针

static关键字的作用:
(1)函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
(2)在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
(3)在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
(4)在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(5)在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

const关键字的作用:
(1)欲阻止一个变量被改变,可以使用const关键字。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;
(3)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

17:刷新缓冲区方式?
换行刷新缓冲区:printf(“\n”);程序结束刷新缓冲区:return 0;

18:类和对象的两个基本概念?
类的作用或概念:用来描述一组具有相似属性的东西的对象的一种数据结构。类中有数据成员的声明和定义,有成员函数的实现代码。
对象就是类的实例化。计算机中想要使用类,只能进行实例化。

19:介绍一下STL,详细说明STL如何实现vector。
STL是标准模版库,由容器,算法,迭代器组成。
STL有以下的一些优点:
(1)可以很方便的对一堆数据进行排序(调用sort());
(2)调试程序时更加安全和方便;
(3)stl是跨平台的,在linux下也能使用。
vector实质上就是一个动态数组,会根据数据的增加,动态的增加数组空间。
什么是容器。如何实现?
容器是一种类类型,用来存储数据
STL有7种主要容器:vector,list,deque,map,multimap,set,multiset.

20:变量的声明和定义有什么区别
变量的声明是告诉编译器我有某个类型的变量,但不会为其分配内存。但是定义会分配了内存。

21:简述#define #endif 和#ifndef的作用
这三个命令一般是为了避免头文件被重复引用。
#ifndef CH_H //意思是如果没有引用ch.h
#define CH_H //引用ch.h
#endif //否则不需要引用

23:C++继承机制?
三类成员的访问控制方式
public:类本身、派生类和其它类均可访问;
protected:类本身和派生类均可访问,其它类不能访问;
private(默认):类本身可访问,派生类和其它类不能访问。
继承成员的访问控制规则
——由父类成员的访问控制方式和继承访问控制方式共同决定
private+public(protectd,private)=>不可访问
public(protected)+public=>public(protected)
public(protected)+protected=>protected
public(protected)+private(默认)=>private
C++中的模板和virtual异同? ==>?
private继承和public继承区别? ==>?

25:头文件的作用是什么?
(1)头文件用于保存程序的声明。
(2)通过头文件可以来调用库函数。因为有些代码不能向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,编译器会从库中提取相应的代码。
(3)如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

26:函数模板与类模板有什么区别?
答:函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。
函数模板是模板的一种,可以生成各种类型的函数实例:
template
Type min( Type a, Type b )
{
return a < b ? a : b;
}
参数一般分为类型参数和非类型参数:
类型参数代表了一种具体的类型
非类型参数代表了一个常量表达式

27:system(”pause”)的作用?
调用DOS的命令,按任意键继续,和getchar()差不多;省去了使用getchar();区别是一个属于系统命令,一个属于c++标准函数库。

28:析构函数和虚函数的用法和作用?
析构函数是类成员函数,在类对象生命期结束的时候,由系统自动调用,释放在构造函数中分配的资源。
虚函数是为了实现多态。含有纯虚函数的类称为抽象类,不能实例化对象,主要用作接口类

Test(int j):pb(j),pa(pb+5) { }
~Test() { cout<<“释放堆区director内存空间1次”; }
析构函数的特点:

  1. 函数名称固定:~类名( ) 2. 没有返回类型,没有参数 3. 不可以重载,一般由系统自动的调用

29:编写一个标准strcpy函数
可以拿10
char * strcpy( char *strDest, const char *strSrc )//为了实现链式操作,将目的地址返回,加3分!
{
assert( (strDest != NULL) &&(strSrc != NULL) );
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘\0’ );
return address;
}

32:继承优缺点
优点:
继承可以方便地改变父类的实现,可以实现多态,子类可以继承父类的方法和属性。
缺点:
破坏封装,子类和父类可能存在耦合。
子类不能改变父类的接口。

34:析构函数的调用次序,子类析构时要调用父类的析构函数吗?
析构函数调用的次序是:先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了
定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;

35:什么是“野指针”?
野指针指向一个已删除的对象或无意义地址的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力避免。造成的主要原因是:指针变量没有被初始化,或者指针p被free或者delete之后,没有置为NULL。

36:常量指针和指针常量的区别?
常量指针:是一个指向常量的指针。可以防止对指针误操作而修改该常量。
指针常量:是一个常量,且是一个指针。指针常量不能修改指针所指向的地址,一旦初始化,地址就固定了,不能对它进行移动操作。但是指针常量的内容是可以改变。

37:sizeof的概念(作用),举例
sizeof是C语言的一种单目操作符,并不是函数。sizeof以字节的形式返回操作数的大小。
(1)sizeof(int **a[3][4])
int **p; //16位下sizeof§=2, 32位下sizeof§=4
总共 34sizeof§
(2)若操作数具有类型char、unsigned char或signed char,其结果等于1。 
(3)当操作数是指针时,sizeof依赖于系统的位数 
(4)当操作数具有数组类型时,其结果是数组的总字节数。 
(5)联合类型操作数的sizeof是其最大字节成员的字节数。
结构类型操作数的sizeof是这种类型对象的总字节数(考虑对齐问题时)。这样做可以提高程序的性能,避免访问两次内存; ??
(6)如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小。

sizeof和strlen()的区别:
①sizeof是运算符,计算数据所占的内存空间;strlen()是一个函数,计算字符数组的字符数;
② sizeof可以用类型作参数;strlen()只能用char*作参数,必须是以‘/0’结束
③ 数组做sizeof的参数不退化,传递给strlen就退化为指针了;
④ sizeof操作符的结果类型是size_t,它在头文件中typedef为unsigned int类型。该类型保证能容纳实现建立的最大对象的字节大小。

38:如果NULL 和0 作为空指针常数是等价的, 那到底该用哪一个?
#define NULL 0
按道理说,null和0,没有区别,但为何要多此一举呢,
(1)什么是空指针常量?
0、0L、’\0’、3 - 3、0 * 17 以及 (void*)0都是空指针常量。
(2)什么是空指针?
如果 p 是一个指针变量,则 p = 0;、p = 0L;、p = ‘\0’;、p = 3 - 3;、p = 0 * 17; 中的任何一种赋值操作之后, p 都成为一个空指针,由系统保证空指针不指向任何实际的对象或者函数。
(3)什么是 NULL?
即 NULL 是一个标准规定的宏定义,用来表示空指针常量。因此,除了上面的各种赋值方式之外,还可以用 p = NULL; 来使 p 成为一个空指针。
(4)空指针指向了内存的什么地方?
标准并没有对空指针指向内存中的什么地方这一个问题作出规定,一般取决于系统的实现。我们常见的空指针一般指向 0 地址,即空指针的内部用全 0 来表示。
空指针的“逻辑地址”一定是0,对于空指针的地址,操作系统是特殊处理的。并非空指针指向一个0地址的物理地址。
在实际编程中不需要了解在我们的系统上空指针到底是一个 0指针还是非0地址,我们只需要了解一个指针是否是空指针就可以了——编译器会自动实现其中的转换,为我们屏蔽其中的实现细节。
(5)可以用 memset 函数来得到一个空指针吗?
这个问题等同于:如果 p 是一个指针变量,那么memset( &p, 0, sizeof§ ); 和 p = 0;是等价的吗?
答案是否定的,虽然在大多数系统上是等价的,但是因为有的系统存在着“非零空指针” (nonzero null pointer),所以这时两者不等价。
(6)可以定义自己的 NULL 的实现吗?兼答"NULL 的值可以是 1、2、3 等值吗?"类似问题
NULL 是标准库中的一个符合上述条件的保留标识符。所以,如果包含了相应的标准头文件而引入了 NULL 的话,则再在程序中重新定义 NULL 为不同的内容是非法的,其行为是未定义的。也就是说,如果是符合标准的程序,其 NULL 的值只能是 0,不可能是除 0 之外的其它值,比如 1、2、3 等。
(7)malloc 函数在分配内存失败时返回 0 还是 NULL?
malloc 函数是标准 C 规定的库函数。在标准中明确规定了在其内存分配失败时返回的是空指针

39:如果NULL定义成#define NULL ((char *)0) 难道不就可以向函数传入不加转换的NULL了吗?
不行。因为有的机器不同类型数据的指针有不同的内部表达。如果是字符指针的函数没有问题, 但对于其它类型的指针参数仍然有问题, 而合法的构造如FILE *fp = NULL;则会失败。
如果定义#define NULL ((void *)0)除了潜在地帮助错误程序运行以外, 这样的定义还可以发现错误使用NULL 的程序。无论如何, ANSI 函数原型确保大多数指针参数在传入函数时正确转换。?????

40:使用非全零的值作为空指针内部表达的机器上, NULL是如何定义的?
跟其它机器一样: 定义为0 。当程序员请求一个空指针时, 无论写“0” 还是“NULL”, 都是有编译器来生成适合机器的空指针的二进制表达形式。因此, 在空指针的内部表达不为0 的机器上定义NULL 为0 跟在其它机器上一样合法:编译器在指针上下文看到的未加修饰的0 都会被生成正确的空指针。

41:NULL 是什么, 它是怎么定义的?
很多人不愿意在程序中到处出现未加修饰的0。因此定义了预处理宏NULL为空指针常数, 通常是0 或者((void *)0) 。希望区别整数0 和空指针0 的人可以在需要空指针的地方使用NULL。

42:用“if§” 判断空指针是否可靠?如果空指针的内部表达不是0 会怎么样?
表达式中要求布尔值时:如果表达式等于0 则认为该值为假。
if§ 等价于if(p != 0)。这是一个比较上下文, 因此编译器可以看出 0 实际上是一个空指针常数, 并使用正确的空指针值。这里没有任何欺骗; 编译器就是这样工作的,并为二者生成完全一样的代码。空指针的内部表达无关紧要。

43:怎样在程序里获得一个空指针?
在指针上下文中的常数0 会在编译时转换为空指针。
char *p = 0;
if(p != 0)

44:空指针到底是什么?
空指针表示“未分配” 或者“尚未指向任何地方” 的指针。
空指针在概念上不同于未初始化的指针。空指针可以确保不指向任何对象或函数; 而未初始化指针则可能指向任何地方。

45:我能否用void** 指针作为参数, 使函数按引用接受一般指针?
不可移植。C 中没有一般的指针的指针类型。void* 可以用作一般指针只是因为当它和其它类型相互赋值的时候, 如果需要, 它可以自动转换成其它类型; 但是, 如果试图这样转换所指类型为void* 之外的类型的void** 指针时, 这个转换不能完成。
46:我有一个char * 型指针刚好指向一些int 型变量, 我想跳过它们。 为什么((int *)p)++; 不行?
类型转换的实质“把这些二进制位看作另一种类型, 并作相应的对待”; ((int *)p)++是一个转换操作符, 根据定义它只能生成一个右值(rvalue)。而右值既不能赋值, 也不能用++ 自增。正确的做法:p = (char *)((int *)p + 1);

47:*p++ 自增p 还是p 所指向的变量?
p++ 和(p++) 等价。要自增p 指向的值, 使用(*p)++, 或者++*p。

48:我想声明一个指针并为它分配一些空间,代码char *p; p = malloc(10)的问题;
你所声明的指针是p, 而不是
p, 当你操作指针本身时, 你只需要使用指针的名字即可:p = malloc(10);

49:int i=7; printf(“%d\n”, i++ *i++);的值?
i++*i++=49

50:枚举和#define 有什么不同?
1):#define 是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
2):一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
3):枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个。

52:声明struct x1 { . . . }; 和typedef struct { . . . } x2; 有什么不同?
第一种形式声明了一个“结构标签”;
第二种声明了一个“类型定义”。
主要的区别是第一种方式定义结构体变量需要写“struct x1”而引用第一种, 而第二种方式定义结构体变量不需要使用struct 关键字。

53:以下的初始化有什么区别?
char a[] = “string literal”; char *p= “string literal”
用作数组初始值, 它指明该数组中字符的初始值。
第二种情况会转化为一个无名的静态字符数组, 可能会存储在只读内存中, 这就是造成它不一定能被修改。第二个声明把p 初始化成指向无名数组的第一个元素。为了编译旧代码, 有的编译器有一个控制字符串是否可写的开关。

54:对于没有初始化的变量的初始值可以作怎样的假定?如果一个全局变量初始值为“零”, 它可否作为空指针或浮点零?
对于具有“静态” 生存期的未初始化全局变量可以确保初始值为零,如果是指针会被初始化为正确的空指针, 如果是浮点数会被初始化为0.0 。
对于局部变量,如果没有显示地初始化, 则包含的是垃圾内容。
用malloc() 和realloc() 动态分配的内存也可能包含垃圾数据, 因此必须由调用者正确地初始化。

55:函数指针的定义是什么?
是一个指向函数的指针。看例子:
A),char * (*fun1)(char * p1,char * p2);//fun1 不是函数名,而是一个指针变量,它指向一个函数。这个函数有两个指针类型的参数,函数的返回值也是一个指针。
B),char * *fun2(char * p1,char * p2);//是个二级指针
C),char * fun3(char * p1,char * p2);//函数的返回值为char *类型

56:int p = NULL 和p = NULL 有什么区别?
int p = NULL;//定义一个指针变量p,其指向的内存里面保存的是int 类型的数据;在定义变量p 的同时把p 的值设置为0×00000000,而不是把p 的值设置为0×00000000
int *p;
p = NULL;
p 赋值为NULL,即给p指向的内存赋值为NULL;但是由于p 指向的内存可能是非法的,所以调试的时候编译器可能会报告一个内存访问错误。
int i = 10;
int *p = &i;
*p = NULL;
在编译器上调试一下,我们发现p 指向的内存由原来的10 变为0 了;而p 本身的值, 即内存地址并没有改变。
57:介绍一下#error 预处理
#error 预处理指令的作用是,编译程序时,只要遇到#error 就会生成一个编译错误提示消息,并停止编译。其语法格式为:
#error error-message
注意,宏串error-message 不用双引号包围。遇到#error 指令时,错误信息被显示,可能同时还显示编译程序作者预先定义的其他内容。

58:用变量a给出下面的定义
a) int a; //一个整型数
b) int *a; //一个指向整型数的指针
c) int **a; //一个指向指针的指针,它指向的指针是指向一个整型数
d) int a[10]; //一个有10个整型数的数组
e) int *a[10]; //一个有10个指针的数组,该指针是指向一个整型数的
f) int (*a)[10]; //一个指向有10个整型数数组的指针
g) int (*a)(int); //一个指向函数的指针,该函数有一个整型参数并返回一个整型数
h) int (*a[10])(int); // 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数
59:分别给出BOOL,int,float,指针变量 与“零值”比较的 if 语句(假设变量名为var)

BOOL型变量:if(!var)
int型变量: if(var0)
float型变量:
const float EPSINON = 0.00001;
if ((x >= – EPSINON) && (x <= EPSINON)???
指针变量:  if(var
NULL)

60:什么是预编译?何时需要预编译?
预编译又称为预处理 , 是做些代码文本的替换工作。
处理 # 开头的指令 , 比如拷贝 #include 包含的文件代码, #define 宏定义的替换 , 条件编译等。
c 编译系统在对程序进行通常的编译之前,先进行预处理。
c 提供的预处理功能主要有以下三 种: 1 )宏定义  2 )文件包含  3 )条件编译

62:iostream与iostream.h的区别
#include <iostream.h>非标准输入输出流
#include 标准输入输出流
有“.h”的就是非标准的,C的标准库函数,无“.h”的,就要用到命令空间,是C++的。

63:namespace的使用
因为标准库非常的庞大,程序员在选择的类的名称或函数名时就很有可能和标准库中的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都被放在名字空间std中。 C++标准程序库中的所有标识符都被定义于一个名为std的namespace中。
1、直接指定标识符。例如std::ostream
2、使用using关键字。
using std::cout;
using std::endl;
3、最方便的就是使用using namespace std;


65:含参数的宏与函数的优缺点
宏: 优点:在预处理阶段完成,不占用编译时间,同时,省去了函数调用的开销,运行效率高
缺点:不进行类型检查,多次宏替换会导致代码体积变大,而且由于宏本质上是字符串替换,故可能会由于一些参数的副作用导致得出错误的结果。
函数:优点:有类型检查,比较安全。缺点:函数调用需要参数、返回地址等的入栈、出栈开销,效率没有带参数宏高
宏与内联函数的区别
内联函数和宏都是在程序出现的地方展开,内联函数不是通过函数调用实现的,是在调用该函数的程序处将它展开(在编译期间完成的);宏同样是;
不同的是:内联函数可以在编译期间完成诸如类型检测,语句是否正确等编译功能;宏就不具有这样的功能,而且宏展开的时间和内联函数也是不同的(在运行期间展开)
66:多态的作用?
(1)可以隐藏实现的细节,使得代码模块化;方便扩展代码;
(2)可以实现接口重用。
67: 类的静态成员和非静态成员有何区别?
静态成员则是属于这个类的
非静态成员是属于每个对象的
68:C++纯虚函数,虚函数,虚函数的实现,什么是虚指针?
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。
virtual void f()=0;//是一个接口,子类必须实现这个接口虚指针或虚函数指针是虚函数的实现细节。带有虚函数的每一个对象都有一个虚指针指向该类的虚函数表。虚函数 :虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态覆盖(Override)
纯虚函数和虚函数的区别是,纯虚函数子类必须实现。
纯虚函数的优点:
(1)可以实现多态特性
(2)定义一个标准的接口,在派生类中必须予以重写以实现多态性。
抽象类 :包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。
多态性可分为两类:静态多态和动态多态。函数重载和运算符重载实现的多态属于静态多态,动态多态性是通过虚函数实现的。
虚函数与构造函数,析构函数,成员函数的关系
为什么基类析构函数是虚函数?
编译器总是根据类型来调用类成员函数。但是一个派生类的指针可以安全地转化为一个基类的指针。这样删除一个基类的指针的时候,C++不管这个指针指向一个基类对象还是一个派生类的对象,调用的都是基类的析构函数而不是派生类的。如果你依赖于派生类的析构函数的代码来释放资源,而没有重载析构函数,那么会有资源泄漏。
为什么构造函数不能为虚函数
虚函数采用一种虚调用的方法。需调用是一种可以在只有部分信息的情况下工作的机制。如果创建一个对象,则需要知道对象的准确类型,因此构造函数不能为虚函数。
如果虚函数是有效的,那为什么不把所有函数设为虚函数?
不行。因为每个虚函数的对象都要维护一个虚函数表,因此在使用虚函数的时候都会产生一定的系统开销,这是没有必要的。
69:面向对象的三个基本特征,并简单叙述之?

  1. 封装:将客观事物抽象成类,每个类对自身的数据和方法。封装可以使得代码模块化,目的是为了代码重用
  2. 继承:子类继承父类的方法和属性,继承可以扩展已存在的代码,目的是为了代码重用
  3. 多态:允许将子类类型的指针赋值给父类类型的指针。
    71:引用与多态的关系?
    引用就是对象的别名。引用主要用作函数的形参。引用必须用与该引用同类型的对象初始化: 引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
    int ival = 1024;int &refVal = ival;
    const 对象的引用只能是const类型的:const int ival = 1024;const int &refVal = ival; 多态是通过虚函数实现的。
    72:指针和引用有什么区别;为什么传引用比传指针安全?如果我使用常量指针难道不行吗?
    (1) 引用在创建的同时必须初始化,保证引用的对象是有效的,所以不存在NULL引用;而指针在定义的时候不必初始化,所以,指针则可以是NULL,可以在定义后面的任何地方重新赋值。
    (2) 引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用;而指针在任何时候都可以改变为指向另一个对象.
    (3) 引用的创建和销毁并不会调用类的拷贝构造函数

因为不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,所以比指针安全。
由于const 指针仍然存在空指针,并且有可能产生野指针,所以还是不安全
73:参数传递有几种方式;实现多态参数传递采用什么方式,如果没有使用某种方式原因是什么?
传值,传指针或者引用
74:拷贝构造函数相关问题,深拷贝,浅拷贝,临时对象等。
深拷贝意味着拷贝了资源和指针
浅拷贝只是拷贝了指针,没有拷贝资源这样使得两个指针指向同一份资源,可能造成对同一份析构两次,程序崩溃。而且浪费时间,并且不安全。
临时对象的开销比局部对象小些。
75:构造函数的特点
构造函数只在建立对象的时候自动被调用一次
构造函数必须是公共的,否则无法生成对象
构造函数只负责为自己的类构造对象
在构造函数中初始化变量
Person::Person( ) : name(“Jack”), age(30)
{

}
76:面向对象如何实现数据隐藏
定义类来实现数据隐藏:
成员函数和属性的类型:
私有成员private
保护成员protected
公共成员public
77:字符指针、浮点数指针、以及函数指针这三种类型的变量哪个占用的内存最大?为什么?
所有指针变量占用内存单元的数量都是相同的。
78:C++是不是类型安全的?
不是。两个不同类型的指针之间可以强制转换.
79:const char*, char const*, char const的区别是什么?
把一个声明从右向左读,
读成指向
char * const cp;//cp是常指针,指向char类型的数据
const char * cp;//cp是char类型的指针,指向const char
char const * p;//C++里面没有const*的运算符,所以const属于前面的类型。
80:什么是模板和宏?模板怎么实现?模板有什么缺点和优点?模版特化的概念,为什么特化?
标准库大量采用了模板技术。比如容器。
模板是一个蓝图,它本身不是类或函数。编译器用模板产生指定的类或函数的特定类型版本。模版的形参分为类型形参和非类型形参;类型形参就是表示类型的形参,跟在关键字typename后非类型形参用来表示常量表达式
作为C++语言的新组成部分,模板引入了基于通用编程的概念。通用编程是一种无须考虑特定对象的描述和发展算法的方法,因此它与具体数据结构无关。但在决定使用C++模板之前,让我们分析一下使用模板的优缺点。
目的:
当被问及引入C++模板的目的时,C++的发明者Bjarne Stroustrup告诉我“这是为了支持类型安全、类容器的有效性和算法的通用性”。
优势和劣势:
使用模板有很多原因,最主要的为了得到通用编程的优点。国际标准化组织(ISO)为C++建立了C++标准库,该标准库功能强大,这证明了模板的重要性。库中涉及算法和容器的部分组成了标准模板库(简称STL)。由于模板的可重用性和可扩展性,你可以利用STL来实现效率很高的代码。
但是模板也有一些不太为人知的缺点。首先,由于C++没有二进制实时扩展性,所以模板不能像库那样被广泛使用。模板的数据类型只能在编译时才能被确定。因此,所有用基于模板算法的实现必须包含在整个设计的头文件中。通过分析标准模板库(STL)的头文件,你可以很清楚的认识到这一点。
另外,由于模板只是最近加入C++标准中,所以有些C++编译器还不支持模板,当使用这些编译器时编译含有模板的代码时就会发生不兼容问题。例如,Mozilla浏览器开发组之所以没有使用模板就是因为交叉平台会导致模板的不兼容。同样的,如果当开发者需要跨越好几个平台而有的平台可能只有老的C++编译器的时候,使用模板也是不明智的。
即使到现在,模板的一些高级特性,例如局部特殊化和特殊化顺序在不同的C++标准实现中也还是不统一的。
尽管如此,结合STL使用模板还是可以大大减少开发时间。模板可以把用同一个算法去适用于不同类型数据,在编译时确定具体的数据类型。
比方说,假设你希望实现一个诸如TCP/IP堆栈所用的开窗重排序机制。这个机制可用于IP数据报和其它数据报格式。通过使用模板后,开窗重排序机制就像流控制那样,无须随所处理数据格式而改变。

81:空指针和悬垂指针的区别?
空指针是指被赋值为NULL的指针;delete指针指向的动态分配对象会产生悬垂指针。
空指针可以被多次delete,而悬垂指针再次删除时程序会变得非常不稳定;
使用空指针和悬垂指针都是非法的,而且有可能造成程序崩溃,如果指针是空指针,尽管同样是崩溃,但和悬垂指针相比是一种可预料的崩溃。
void* 类型指针:通用变体类型指针;可以不经转换,赋给其他指针,函数指针除外;malloc返回的就是void*类型。

NULL指针:是一个标准规定的宏定义;
#define NULL ((void *) 0) 用来表示空指针常量;

零指针:指针值为0,零值指针,没有存储任何内存地址的指针;可以是任意一种指针类型,eg:void * ;int * ;double *;

空指针:指针赋值为0;0*7;3-3等之后,指针即变成空指针;即:空指针不指向任何实际的对象或者函数;NULL指针和零指针都是空指针。
野指针:指向垃圾内存的指针;(1)指针变量没有初始化(2)指针被delete或者free之后没有置为空(3)指针操作超越了变量的范围。【注:使用没有被初始化的指针(野指针)的结果是未定义的。】
“野指针”的成因主要有两种:
(1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如
char *p = NULL;
char *str = (char *) malloc(100);
(2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
free和delete只是把指针所指的内存给释放掉,但并没有把指针本身干掉。free以后其地址仍然不变(非NULL),只是该地址对应的内存是垃圾,p成了“野指针”。如果此时不把p设置为NULL,会让人误以为p是个合法的指针。
如果程序比较长,我们有时记不住p所指的内存是否已经被释放,在继续使用p之前,通常会用语句if (p != NULL)进行防错处理。很遗憾,此时if语句起不到防错作用,因为即便p不是NULL指针,它也不指向合法的内存块。

char *p = (char *) malloc(100);
 strcpy(p, “hello”);
 free(p);         // p 所指的内存被释放,但是p所指的地址仍然不变
 …
 if(p != NULL)      // 没有起到防错作用
 {
    strcpy(p, “world”);      // 出错

}
(3)指针操作超越了变量的作用范围。这种情况让人防不胜防,示例程序如下:

class A
{
public:
void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void)
{
A p;
{
A a;
p = &a; // 注意 a 的生命期
}
p->Func(); // p是“野指针”
}
悬垂指针(dangling pointer)一般是说指向已经被释放的自由区内存(free store)的指针,野指针(wild pointer)则一般是未经初始化的指针。前者曾经有效过,后者从未有效过。
(a)指针数组和数组指针,函数指针和指针函数相关概念
指针数组:用于存储指针的数组
int
a[4]
元素表示:*a[i]
数组指针:指向数组的指针
int (*a)[4]
元素表示:(*a)[i]
指针函数:函数返回类型是某一类型的指针,int f(x,y);
指针函数与函数指针表示方法的不同。最简单的辨别方式就是看函数名前面的指针
号有没有被括号()包含,如果被包含就是函数指针,反之则是指针函数。
函数指针:是指向函数的指针变量,即本质是一个指针变量。
int (f) (int x); / 声明一个函数指针 */ 类型说明符 (指针的变量名)(参数)
f=func; /
将func函数的首地址赋给指针f */
指向函数的指针包含了函数的地址
指针的指针: 例如:char ** cp;
如果有三个星号,那就是指针的指针的指针,依次类推。 指针的指针需要用到指针的地址。
char c=‘A’;
char *p=&c;
char **cp=&p;
通过指针的指针,不仅可以访问它指向的指针,还可以访问它指向的指针所指向的数据:
char *p1=*cp;
char c1=**cp; 指向指针数组的指针: char *Names[]={ Bill,Sam,0};
char **nm=Names;
while(*nm!=0) printf(%s\n,*nm++);
先用字符型指针数组Names的地址来初始化指针nm。每次printf()的调用都首先传递指针nm指向的字符型指针,然后对nm进行自增运算使其指向数组的下一个元素(还是指针)。
82:什么是智能指针?
当类中有指针成员时,一般有两种方式来管理指针成员:
(1)每个类对象都保留一份指针指向的对象的拷贝;
(2)使用智能指针,从而实现指针指向的对象的共享。实质是使用计数器与对象相关联,这样做可以保证对象正确的删除,避免垂悬指针。
每次创建类的新对象时,初始化指针并将引用计数置为1;
当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;
对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数,并增加右操作数所指对象的引用计数;
调用析构函数时,构造函数减少引用计数。
83:C++空类默认有哪些成员函数?
默认构造函数、析构函数、复制构造函数、赋值函数
84:哪一种成员变量可以在一个类的实例之间共享?
答:static静态成员变量
85:什么是多态?多态有什么作用?如何实现的?多态的缺点?
多态就是一个接口,多种方法。所以说,多态的目的则是为了实现接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
C++的多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或重写。而重载则是允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同。编译器会根据函数列表的不同,而生成一些不同名称的预处理函数,来实现同名函数的重载。但这并没有体现多态性。
多态与非多态的实质区别就是函数的地址是运行时确定还是编译时确定。如果函数的调用在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的。而如果函数调用的地址在运行时才确定,就是动态的。
最常见的用法就是声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同而实现不同的方法。如果没有使用虚函数的话,即没有利用C++多态性,则利用基类指针调用相应的函数的时候,将总被限制在基类函数本身,而无法调用到子类中被重写过的函数

86:虚函数表解析和内存布局
虚函数表
虚函数是通过一张虚函数表来实现的。就像一个地图一样,指明了实际所应该调用的函数的地址。
这里我们着重看一下这张虚函数表。C++的编译器保证了虚函数表的指针存在于对象实例中最前面的位置(为了性能)。因此我们可以通过对象实例的地址得到这张虚函数表,然后通过遍历其中函数指针,并调用相应的函数。
为什么可以由父类的指针调用子类的对象的虚函数:
Derive d;//Derive 是Base的子类
Base *b1 = &d;//这必须使用父类的指针
b1->f(); //Derive::f()

87:公有继承、受保护继承、私有继承
1)公有继承
派生类对象可以访问基类中的公有成员
派生类的成员函数可以访问基类中的公有和受保护成员
可以通过派生类对象访问但不能修改基类受保护的成员
2)私有继承
基类的成员只能被直接派生类的成员访问,无法再往下继承;
3)受保护继承
基类的成员也只被直接派生类的成员访问,无法再往下继承。

88:有哪几种情况只能用构造函数初始化列表而不能用赋值初始化?
答:const成员,引用成员

89:C++如何阻止一个类被实例化?一般在什么时候将构造函数声明为private?
类定义为抽象基类或者将构造函数声明为private;
不允许类外部创建类对象,只能在类内部创建对象

90:类使用static成员的优点,如何访问?
(1)static 成员的名字是在类的作用域中,因此可以避免与其他类的成员或全局对象名字冲突;
(2)可以实施封装。static 成员可以是私有成员,而全局对象不可以;
(3) static 成员是与特定类关联的,可清晰地显示程序员的意图。

91:static数据成员和static成员函数
(1)static数据成员:
static数据成员独立于该类的任意对象而存在;static数据成员(const static数据成员除外)在类定义体内声明,必须在类外进行初始化。不像普通数据成员,static成员不能在类的定义体中初始化,只能在定义时才初始化。 static数据成员定义放在cpp文件中,不能放在初始化列表中。const static成员可就地初始化。
变量定义:用于为变量分配存储空间,还可为变量指定初始值。程序中,变量有且仅有一个定义。
变量声明:用于向程序表明变量的类型和名字。
(3)static成员函数: 在类的外部定义,static成员函数没有this形参,它可以直接访问所属类的static成员,不能直接使用非static成员。,const表明不能改变对象,因为static成员不是任何对象的组成部分,static成员本身就不会改变对象,所以static成员函数不能被声明为const。同时,static成员函数也不能被声明为虚函数。
不能用const的原因:
这是C++的规则,const修饰符意味着函数不能修改成员变量的值,该函数必须是含有this指针的类成员函数,函数调用方式为thiscall,而类中的static函数本质上是全局函数,调用规约是__cdecl或__stdcall,不能用const来修饰它。一个静态成员函数访问的值是其参数、静态数据成员和全局变量,而这些数据都不是对象状态的一部分。而对成员函数中使用关键字const是表明:函数不会修改该函数访问的目标对象的数据成员。既然一个静态成员函数根本不访问非静态数据成员,那么就没必要使用const了
不能声明为虚函数的原因:
虚函数是为了实现多态而提出的概念,多态就是用子类的对象指针调用函数时是调用子类函数还是调用基类函数,当基类中的函数定义为虚函数时,再用子类中的对象指针调用时,就调用的是子类函数。static定义的函数是属于类的,无法用对象或对象指针来调用,也就不存在多态的概念,因而静态函数若定义为虚函数就是非法的。

92:C++的内部连接和外部连接
编译单元:当编译cpp文件时,预处理器首先递归包含头文件,形成一个编译单元。这个编译单元会被编译成为一个与cpp文件名同名的目标文件(.o或是.obj)。连接程序把不同编译单元中产生的符号联系起来,构成一个可执行程序。
内部连接:如果一个名称对于它的编译单元来说是局部的,并且在连接时不会与其它编译单元中的同样的名称相冲突,那么这个名称有内部连接:
a)所有的声明
b)名字空间(包括全局名字空间)中的静态自由函数、静态友元函数、静态变量的定义
c)enum定义
d)inline函数定义(包括自由函数和非自由函数)
e)类的定义
f)名字空间中const常量定义
g)union的定义
外部连接:在一个多文件程序中,如果一个名称在连接时可以和其它编译单元交互,那么这个名称就有外部连接。
以下情况有外部连接:
a)类非inline函数总有外部连接。包括类成员函数和类静态成员函数
b)类静态成员变量总有外部连接。
c)名字空间(包括全局名字空间)中非静态自由函数、非静态友元函数及非静态变量

95:C++里面是不是所有的动作都是main()引起的?如果不是,请举例。
比如全局变量的初始化,就不是由main函数引起的:
class A{};
A a; //a的构造函数限执行
int main() {}
96:异常框架
异常存在于程序的正常功能之外,并要求程序立即处理。 C++ 的异常处理包括:
1.throw 表达式,错误检测部分使用这种表达式来说明遇到了不可处理的错误。
2.try 块,错误处理部分使用它来处理异常。try 语句块以 try 关键字开始,并以一个或多个 catch 子句结束。在 try 块中执行的代码所抛出 (throw)的异常,通常会被其中一个 catch 子句处理。
3.由标准库定义的一组异常类,用来在 throw 和相应的 catch 之间传递有关的错误信息。
throw 表达式:
if (!item1.same_isbn(item2))
throw runtime_error(“Data must refer to same ISBN”);
try 块:try {program-statements} catch (exception-specifier) {handler-statements} catch (exception-specifier) {handler-statements}
函数在寻找处理代码的过程中退出在复杂的系统中,程序的执行路径也许在遇到抛出异常的代码之前,就已经经过了多个 try 块。
抛出一个异常时,首先要搜索的是抛出异常的函数。如果没有找到匹配的 catch,则终止这个函数的执行,并在调用这个函数的函数中寻找相配的 catch。如果仍然没有找到相应的处理代码,该函数同样要终止,搜索调用它的函数。直到找到适当类型的 catch 为止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值