C++基础知识

1. 结构体占用空间


struct Demo{
	int a;
	double b;
	char c;
};

Demo占用24个字节,每个变量都按照占用字节最大的变量来对齐。double占用8个字节(64位),int本来占4个字节,为了与double对齐,也需要8个字节。char本来占1个字节,也为了对齐,也需要8个字节,所以总共需要24个字节。

struct Demo{
	int a;
	double b;
	char c[10];
};

Demo占用32个字节,与上例相同,int和double各占8个字节,char数组本来占10个字节,但是为了与double对齐,必须要是8的倍数,所以是16个字节,所以整个结构体占用32个字节。

struct Demo{
	char a;
	int* b;
};

这个结构体在64位系统下,占16字节。指针类型占8字节,为了字节对齐,所以a也占8字节,总共占16字节。

总结:假设结构体有n个变量,每个变量原本占用字节数是a1,a2,…,an。这其中占用空间最大的基础类型(包括指针类型)的长度是d,那么每个变量实际占用空间必须是d的倍数,不足的要补满。最终的结构体的占用长度也必然是d的倍数。

字节对齐:

对齐内存的好处简单的说就是加速内存访问。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。
比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
CPU一次访问时,要么读0x01~0x04,要么读0x05~0x08…硬件不支持一次访问就读到0x02~0x05
例:如果0x02~0x05存了一个int,读取这个int就需要先读0x01~0x04,留下0x02~0x04的内容,再读0x05~0x08,留下0x05的内容,两部分拼接起来才能得到那个int的值,这样读一个int就要两次内存访问,效率就低了。
注意

  1. long占用4个字节,long long占用8个字节。
  2. string类型不同的编译器占用空间,即sizeof(string)不同。
  3. 指针类型数据也要占空间,32位系统占32位(4字节),64位系统占64位(8字节)。
  4. char a=1,此时a已转化为int。所以sizeof(a+1)=4。

2. define用法和a++用法


define例子:

#define A 4+5
#define B A*A
那么B*B=?
#define只会对变量作替换,不会改变运算符的优先级。
B*B=A*A*A*A=4+5*4+5*4+5*4+5=69

a++与++a的区别,前者是先用a的值操作,然后再a+1,后者正好相反。

int a=4;
(1)a +=  (a++);
(2)a+=(++a).

(1)先用a,再++,左边a=4,右边a++后等于5,最终相加等于9;
(2)先++,再用a,右边a加1后等于5,此时左边的a也等于5,相加后等于10。

3. 不连续的枚举类型


enum TYPE{
	type1,
	type2,
	type3,
	type4 = 12,
	type5,
	type6 = 'a',
	type7,
};
int a = type1;
int b = type5;
int c = type7;

a,b,c的值分别是0,13,98。

4. 数组读取速度对比


下面两段代码哪个运行快?

#define A_SIZE 10000000
#define B_SIZE 1000

int a[A_SIZE], b[B_SIZE];

void t1()
{
	int i,j;
	for(i=0; i<A_SIZE; ++i)
	{
		for(j=0; j<B_SIZE; ++j)
		{
			b[j]++;
		}
	}
}

void t2()
{
	int i,j;
	for(i=0; i<B_SIZE; ++i)
	{
		for(j=0; j<A_SIZE; ++j)
		{
			a[j]++;
		}
	}
}

t1执行快。这两段程序是对数组b和a逐次加1。
数组b的size是1000,每个依次加了10000000次。
数组a的size是10000000,每个依次加了1000次。
显然读取大数组的速度要慢,所以t1快。
实验得知,t1执行时间为1219ms,t2执行时间是4725ms

5. static关键字


四个作用:
(1) static关键字用于修饰全局变量时,变量放在全局数据区,此变量只能在本文件内使用;
在c++11中已经不推荐使用这种方法了,一般用匿名命名空间来限定。

(2) 修饰函数时,此函数只能在本文件使用,其它文件中可以定义相同名字的函数,不会发生冲突;

(3) 修饰局部变量时(在某个函数内部),变量存放在全局数据区,但作用范围只在该函数内,其值下次调用时,仍维持上次的值;

(4) 修饰类中的成员变量和成员函数时,这个变量属于整个类,而不是属于单个实例。用::访问。

全局变量名可以与局部变量名相同。作用域互不干扰。
例子:

int x=4;

void force()
{
	static int x=1;
	x *= x+1;
	printf("%d\n", x);
}

int main()
{
	int i=0;
	for(i=0; i<x; i++)
	{
		force();
	}
	return 0;
}

输出:

2
6
42
1806

全局变量x=4,这个变量只会在for循环条件中有效,所以循环4次。这里全局变量x修饰为static,结果一样。
局部变量x因为是static变量,所以存放在全局数据区,下次使用到x时,还是用上一次的值。其作用域只在force函数中。
x *= x+1等价于x=x*(x+1),先计算右边,然后再相乘。

如果static修饰一个静态结构体的实例,只会在第一次调用构造函数,以后不会再调用。
class A;
int ai = 10;
A* getA(int a) {
static A a(ai);
return &a;
}
多次调用getA,只会在第一次初始化实例,以后都不会再初始化。

6. delete[]和delete的区别


delete[]和new[]配对使用;
delete与new配对使用;
对于基本数据类型数组,delete[]和delete都可以。

int* a = new int[10];
delete a;
delete[] a;

上述两种都可以,分配简单类型内存时,内存大小已经确定,系统可以记忆并且进行管理,在析构时,系统并不会调用析构函数。

A* a =new A[3];
delete[] a;

此处只能用delete[],为自定义类型分配和回收空间,需要调用所有对象的析构函数,如果是delete,只会析构A[0],而用delete[],则会析构A[0]~[2].

7.堆栈


一个由C/C++编译的程序占用的内存分为以下几个部分 :

1、栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 栈由系统自动分配,速度较快。但程序员是无法控制的。

2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。如malloc和new出来的空间。堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

3、全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。

4、文字常量区:常量字符串就是放在这里的。程序结束后由系统释放。

5、程序代码区:存放函数体的二进制代码。

下面是一段程序的内存占用分析:

 int  a = 0;   全局初始化区    
  char   *p1;   全局未初始化区    
  main()    
  {    
	  int b;   栈    
	  char s[] = "abc";   栈    
	  char *p2;   栈    
	  char *p3   =   "123456";   "123456\0"在常量区,p3在栈上
	  static int c = 0;   全局(静态)初始化区
	  p1 = (char *)malloc(10);
	  p2 = new char[20];  分配得来得10和20字节的区域就在堆区,注意p1、p2本身是在栈中
	  strcpy(p1,"123456");123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方
  }

8. 继承多态实现原理


参见博文:
http://blog.csdn.net/okiwilldoit/article/details/50523988
http://blog.csdn.net/okiwilldoit/article/details/78283131
构造函数不能是虚函数,static函数也不能是虚函数,析构函数一般要设为虚函数。

9. const、mutable、volatile使用方法


(1)修饰变量
const T*:指向的内存空间无法修改,但指针可以指向其他内存块
T* const:指针本身是常量,无法修改其指向,但所指向的内存可以修改。
const T* const:指针指向和指向的内存都不能修改。
如下代码:

char* s1 = "abc";
char* s2 = "123";

const char* a = s1;
printf("a=%s\n", a);
a = s2;//可以改变其指向
printf("a=%s\n", a);

int at = 10;
const int* p_at = &at;
//*p_at = 2;//编译出错,指向的内存不能修改
at = 2;//at可以修改
printf("p_at=%d\n", *p_at);

int at2 = 1;
int* const b = &at;
printf("b=%d\n", *b);
*b = 3;//可以修改指向的内存空间
printf("b=%d\n", *b);
//b = &at2;//编译出错,不能改变其指向

const int* const c = &at;
//c = &at2;//编译出错,不能改变其指向
//*c = at2;//编译出错,指向的内存不能修改

(2)修饰普通函数的参数
const修饰参数不能在函数修改其值

void fun(const int a)
{
	a = 0;//编译出错
}

(3)const修饰成员函数
const修饰的成员函数不能修改任何的成员变量
const成员函数不能调用非const成员函数,因为非const成员函数可以会修改成员变量。

详见博文:
http://blog.csdn.net/okiwilldoit/article/details/49151821

mutable和volatile关键字

volatile的本意是“易变的”。volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
const 可以是 volatile (如只读的状态寄存器)
指针可以是 volatile

mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词。
在C++中,mutable也是为了突破const的限制而设置的。
被mutable修饰的变量(mutable只能修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中

10. C++类的大小——sizeof(class)


  1. sizeof(class)是这个class的一个实例的大小,因此只会计算类的实例的大小。
  2. 空类占1字节,空类也会被实例化,所以编译器会给空类隐含的添加一个字节。仅包含普通成员函数的类也是占1字节。
  3. 成员变量的占用大小与struct相同。
  4. 包含虚函数的类,会增加一个虚函数指针,即加一个指针大小
  5. 构造函数,析构函数,普通成员函数不占空间
  6. 静态成员变量和函数是属于整个类,静态数据成员被编译器放在程序的一个global data members中,但是它不影响实例的大小,即不占空间。
  7. 普通的继承,派生类的大小是派生类自身的大小,加上基类成员变量的大小。实际大小就是基类和子类的所有数据成员大小,再加上一个虚表指针(如果有虚函数),基类的虚表指针不能算
  8. 多继承,派生类大小要加上所有基类的大小。
  9. 虚继承,派生类中会有一个指向虚基类表的指针,另外,对于虚继承中,派生类中存在一个或多个虚函数的时候,它本身就有一个虚表,指向自己的虚表。基类也会有自己的虚表指针,都要加上。
class CBase
{
public:
    CBase(void);//不占空间
    virtual ~CBase(void);//只要包含虚函数,就会有一个虚表指针,8字节
	virtual void get()//函数本身不占空间
	{
		printf("CBase");
	}

	void set();//不占空间

	static void sget();//不占空间

private:
    int   a;//8字节
    char *p;//8字节
	static double c;//静态成员属于整个类,不占空间
};

在64位操作系统下,指针占用8字节,sizeof(CBase) = 24字节。

在CBase基础上派生出一个类(普通继承):

class Derived1 : public CBase{
private:
	int a;//8字节
	char* p;//8字节

public:
	void get()
	{
		printf("Derived1");
	}
	virtual void sys();//虚函数,虚表指针,8字节
};

sizeof(Derived1) = 40字节,等于子类的大小(24字节)加上基类成员变量(两个变量,16字节)的大小。

在CBase基础上派生出一个类(虚继承):

class Derived2 : virtual public CBase{//虚基类表指针,8字节
private:
	int a;//8字节
	char* p;//8字节

public:
	void get();
	void set();
	virtual void sys();//虚函数,有自己的虚表指针,与基类独立,各占8字节
};

sizeof(Derived2) = 56字节,等于子类大小(24字节)加上基类成员变量(两个变量,16字节)的大小,再加上基类虚表指针8字节,和子类中指向虚基类表指针,总共56字节。

说明:对象的复制和赋值,可以用=和拷贝构造函数实现,但是用=赋值时,成员变量不能是动态分配的数据(malloc和new出来的),赋值时可能会出现严重后果。

11. C++类型转换


C++中的类型转换分为两种:隐式类型转换;显式类型转换。

而对于隐式变换,就是标准的转换,在很多时候,不经意间就发生了,比如int类型和float类型相加时,int类型就会被隐式的转换位float类型,然后再进行相加运算。下面重点总结显式转换。

在标准C++中有四个类型转换符:static_cast、dynamic_cast、const_cast、reinterpret_cast

四种转化格式都是:xxxx_cast <type-id> (expression)

(1)static_cast
将expression转换为type-id类型,主要用于非多态类型之间的转换,不提供运行时的检查来确保转换的安全性。主要在以下几种场合中使用:
用于类层次结构中,基类和子类之间指针和引用的转换;
当进行上行转换,也就是把子类的指针或引用转换成父类表示,这种转换是安全的;
当进行下行转换,也就是把父类的指针或引用转换成子类表示,这种转换是不安全的,也需要程序员来保证;
用于基本数据类型之间的转换,如把int转换成char,把int转换成enum等等,这种转换的安全性需要程序员来保证;
把void指针转换成目标类型的指针,是非常不安全的;

int i;
float f = static_cast<float>(i);

(2)dynamic_cast
将expression转换为type-id类型,
type-id必须是类的指针、类的引用;
如果type-id是指针类型,那么expression也必须是一个指针;
如果type-id是一个引用,那么expression也必须是一个引用。
dynamic_cast转换符只能用于含有虚函数的类。

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。如:

class A{
	...
};

class B : A{
	...
};

int main
{
	B* b = new B;
	A* a = dynamic_cast<A*>(b);//上行转换是安全的

	A* a1 = new B;//作用与dynamic_cast一样,同样是上行转换
}

static_cast和dynamic_cast区别:
dynamic_cast操作符会在运行期对可疑的转型操作进行安全检查,而static_cast操作符不会进行安全检查;
dynamic_cast仅对指针或者引用有效,并且只能作用于含有虚函数的类(否则会编译出错),而static_cast可施加与任何类型;
(3)const_cast
const_cast用来将类型的const、volatile和__unaligned属性移除。
用于取出const属性,把const类型的指针变为非const类型的指针,如:

const int constant = 21;
const int* const_p = &constant;
int* modifier = const_cast<int*>(const_p);
*modifier = 7;

const_p经过const_cast转换后,就去除了const属性,指向新的指针modifier就可以修改内存值了。

(4)reinterpret_cast
interpret是解释的意思,reinterpret即为重新解释;
允许将任何指针类型转换为其它的指针类型;听起来很强大,但是很不靠谱。
它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针,在实际开发中,先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原来的指针值;特别是开辟了系统全局的内存空间,需要在多个应用程序之间使用时,需要彼此共享,传递这个内存空间的指针时,就可以将指针转换成整数值,得到以后,再将整数值转换成指针,进行对应的操作。如:

int i; 
char *ptr = "hello freind!";
i = reinterpret_cast<int>(ptr);

这个转换方式很少使用。

(5)explicit关键字
explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。
如果被声明为explicit,说明该构造函数只能被显式调用。它是用来防止隐式转换的。
explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了。

class Test1
{
public:
    Test1(int n)
    {
        num=n;
    }//普通构造函数
private:
    int num;
};
class Test2
{
public:
    explicit Test2(int n)
    {
        num=n;
    }//explicit(显式)构造函数
private:
    int num;
};
int main()
{
    Test1 t1=20;//隐式调用其默认拷贝构造函数,成功
    Test2 t2=20;//编译错误,不能隐式调用其构造函数
    Test2 t2(20);//显式调用成功
    return 0;
}

12. 大小端


Big-Endian和Little-Endian的定义如下:

  1. Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  2. Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
    举一个例子,比如数字0x12 34 56 78在内存中的表示形式为:
    1)大端模式:
    低地址 -----------------> 高地址
    0x12 | 0x34 | 0x56 | 0x78
    2)小端模式:
    低地址 ------------------> 高地址
    0x78 | 0x56 | 0x34 | 0x12
    可见,大端模式和字符串的存储模式类似。
    假设上述数字从地址0x0000开始存放,如果采用Big-Endian模式存储,那么0x0000存储的是0x12,如果采用小端模式,那么0x0000存储的是0x78。
    我们常用的X86结构是小端模式,而网络字节序是大端模式
    一般操作系统都是小端,而通讯协议是大端的。另外,Java和所有的网络通讯协议都是使用Big-Endian的编码。

13.union使用


共用体,也叫联合体,在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据,这些数据共享同一段内存,以达到节省空间的目的union变量所占用的内存长度等于最长的成员的内存长度
union与struct比较
struct student
{
char mark;
long num;
float score;
};
sizeof(struct student)的值为12字节
union test
{
char mark;
long num;
float score;
};
sizeof(union test)的值为4
因为共用体将一个char类型的mark、一个long类型的num变量和一个float类型的score变量存放在同一个地址开始的内存单元中,而char类型和long类型所占的内存字节数是不一样的,但是在union中都是从同一个地址存放的,也就是使用的覆盖技术,这三个变量互相覆盖,而这种使几个不同的变量共占同一段内存的结构,称为“共用体”类型的结构。

由于union里面的东西共享内存,所以不能定义静态、引用类型的变量。
因union中的所有成员起始地址都是一样的,所以&a.mark、&a.num和&a.score的值都是一样的。

union的一个用法就是可以用来测试CPU是大端模式还是小端模式:

#include <iostream>
using namespace std;

void checkCPU()
{
    union MyUnion{
        int a;
        char c;
    }test;
    test.a = 1;
    if (test.c == 1)
        cout << "little endian" <<endl;
    else cout << "big endian" <<endl;
}

int main()
{
    checkCPU();
    return 0;
}

一个简单的例子:

union ua {
     char c;
     int i;
};

ua u;
u.i = 90;
printf("%c\n", u.c);

输出Z,因为c和i共用内存,c就是字符Z,Z的ASCII码是90。
复杂的例子:

union Test{  
    struct{  
        int x;  
        int y;  
        int z;  
    }s;  
    int k;  
}myUnion;  
  
int main()  
{  
    myUnion.s.x = 4;
    myUnion.s.y = 5;  
    myUnion.s.z = 6;  
    myUnion.k = 0;  
    cout<< myUnion.s.x <<endl;  
    cout<< myUnion.s.y <<endl;  
    cout<< myUnion.s.z <<endl;  
    cout<< myUnion.k <<endl;
}  

输出结果:0 5 6 0
union类型是共享内存的,以size最大的结构作为自己的大小。
每个数据成员在内存中得其实地址是相同的。
这样的话,myunion这个结构就包含u这个结构体,而大小也等于u这个结构体的大小,
在内存中的排列为声明的顺序x,y,z从低到高。

赋值的时候,在内存中,就是x的位置放置4,y的位置放置5,z的位置放置6,现在对k赋值,对k的赋值因为是union,要共享内存,所以从union的首地址开始放置,首地址开始的位置其实是x的位置,这样原来内存中x的位置就被k所赋的值代替了,就变为0了,这个时候要进行打印,就直接看内存里就行了,x的位置也就是k的位置是0,而y,z的位置的值没有改变。

14. 类的构造函数在初始化成员的两种方式


类的构造函数在初始化成员时,有以下两种方式:
方式一:赋值初始化,通过在函数体内进行赋值初始化

CSomeClass::CSomeClass()
{ 
   x=0;
   y=1;
} 

方式二:列表初始化,在冒号后使用初始化列表进行初始化

CSomeClass::CSomeClass() : x(0), y(1)
{
} 

这两种方式的主要区别在于:
对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。

列表初始化是给数据成员分配内存空间时就进行初始化,
就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式
(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值
就是说初始化这个数据成员此时函数体还未执行。

在大多数情况下,使用两者并没有什么太大区别。但是,第二种方式会更好些。原因从必要性和效率两方面来解释。
1)从必要性:
a. 成员是类或结构,且构造函数带参数:成员初始化时无法调用缺省(无参)构造函数
b. 成员是常量或引用:成员无法赋值,只能被初始化
如下面的例子:

class CMember { 
public: 
	CMember(int x) { ... } 
};

因为Cmember有一个显式声明的构造函数,编译器不产生一个缺省构造函数(不带参数),所以没有一个整数就无法创建Cmember的一个实例。
如果Cmember是另一个类的成员,你怎样初始化它呢?你必须使用成员初始化列表。

class CMyClass { 
	CMember m_member;
	const int a;
	int &b;
 
public: 
	CMyClass(int i, int j);
 
}; 

下面的代码并不能通过编译,因为常量初始化时必须赋值,它的值是不能再改变的,与常量一样引用初始化也需要赋值,定义了引用后,它就和引用的目标维系在了一起,也是不能再被赋值的。
CMyClass::CMyClass(int i, int j)
{
m_member(i);
a = 1;
b = 2;
}

//必须使用成员初始化列表
CMyClass::CMyClass(int i, int j) : m_member(2), a(i), b(j)
{
}

2)第二个原因是出于效率考虑,当成员类具有一个缺省的构造函数和一个赋值操作符时,函数体初始化会多一次赋值操作。

MFC的Cstring提供了一个完美的例子。假定你有一个类 CmyClass具有一个Cstring类型的成员m_str,你想把它初始化为 "yada yada. "。你有两种选择:

// 使用赋值操作符
CMyClass::CMyClass() { 
	// CString::operator=(LPCTSTR); 
	m_str = _T( "yada yada "); 
} 

//使用类成员列表 
// and constructor CString::CString(LPCTSTR) 
CMyClass::CMyClass() : m_str(_T("yada yada "))
{
}

在第一个例子中编译的代码将调用CString::Cstring来初始化m_str,这在控制到达赋值语句前完成。
在第二个例子中编译器产生一个对CString::CString(LPCTSTR)的调用并将 "yada yada"传递给这个函数。

结果是在第一个例子中调用了两个CString函数(构造函数和赋值操作符),而在第二个例子中只调用了一个函数。
总结来说,函数体内初始化,可能会多一次赋值操作符运算,因此大多数情况下采用初始化列表方式比较好。

但是!在我们使用初始化列表进行初始化时,要注意一个特性,它是关于C++初始化类成员的。它们是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
例如:

class CMyClass { 
	CMyClass(int x, int y); 
	int m_x; 
	int m_y; 
}; 

CMyClass::CMyClass(int i) : m_y(i), m_x(m_y) 
{
}

**按照声明的顺序,编译器先初始化m_x,然后是m_y,,因为它们是按这样的顺序声明的。**结果是m_x将有一个不可预测的值。

15.#undefine的用法、#pragma once用法


undefine表示取消注释之前定义的宏

#define MAX 123
int max = MAX
#undefine MAX
int min = MAX - 10; //这里会编译报错

为了避免同一个文件被include多次,被编译两次,有两种方式:
一种是#ifndef方式,
一种是#pragma once方式(在头文件的最开始加入)

16. 内部类Class A::B


class B:A是类的继承关系,即A类是B类的基类。

Class A::B为类的嵌套关系,即A类是B类内部的类,双冒号为作用域。

注意,此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。

如果内部类定义在public,则可通过 外部类名::内部类名 来定义内部类的对象。

如果定义在private,则外部不可定义内部类的对象,这可实现“实现一个不能被继承的类”问题。

内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。

17. STL容器的元素删除


对于c++里面的容器, 我们可以使用iterator进行方便的遍历。
但是当我们通过iterator对vector等进行修改时, 我们就要小心了, 因为操作往往会导致iterator失效, 之后的行为都变得不可预知. 比如:

//!!!!错误写法!!!!
vector a = {12, 23, 34, 45, 56, 67, 78, 89};
for (auto iter = a.begin(); iter != a.end(); ++iter) {
if (*iter > 30) {
a.erase(iter);
}
}
输出新的vector:
12 23 45 67 89
明显结果是不对的。因为vector删除后会导致iterator失效,再去删除,就会fatal了,erase函数会返回下一个iterator, 因此我们可以使用如下的方法:

for (auto iter = a.begin(); iter != a.end();) {
    if (*iter > 30) {
        iter = a.erase(iter);
    }
    else {
        ++iter;
    }
}

对于map, 也是类似的。

map<int, int> a = {{1, 12}, {2, 23}, {3, 34}, {4, 45}, {5, 56}, {6, 67}};
for (auto iter = a.begin(); iter != a.end(); ) {
    if (iter->second > 30) {
        iter = a.erase(iter);
        //a.erase(iter++);//也可以使用这种方式
    } else {
          ++iter;
    }
}

对于map的erase为什么可以使用a.erase(iter++);这种写法呢?
因为iter在++之前,就已经拷贝了一个iter传递给erase函数,所以erase删除的还是当前iter。而进行下一次遍历时,iter已经进行了++操作。

18. 二维数组传参


特别注意,二维数组不能直接用int** 作为形参
数组名作为形参,形参声明一定要给出第二个维度的大小,否则编译不过。
二维数组的本质是指针数组,数组元素是指针,而一维数组也是个指针,所以必须要指定第二个维度的大小。
两种方法:

第一种方法:
void func1(int iArray[][10])
{
}

第二种方法:
void func2(int (*pArray)[10])
{
}

int main()
{
    int array[10][10];
    func1(array);
    func2(array);
}

19.匿名命名空间

特性:写在cpp文件中的匿名命名空间中的变量只能在本文件中使用,此作用与static变量作用类似,在C++最新特性中,建议使用匿名命名空间代替static。
注意:写在头文件中的匿名命名空间变量,被其他cpp include后,仍可以访问。
下面是示例代码:
hello.h

namespace hello {
static int kStaticValue = 1000;
namespace {
	int kValue = 10;
}

class Hello {
public:
	void print();
};

}

hello.cpp

#include "hello.h"
#include <iostream>

using namespace std;

namespace hello{
static int kStaticValueCpp = 23;
namespace {
int kValueCpp = 46;
}

void Hello::print()
{
	cout << "kStaticValueCpp:" << kStaticValueCpp << ",kStaticValue:" << kStaticValue << endl;
	cout << "kValueCpp:" << kValueCpp << ",kValue:" << kValue << endl;
}
}

主函数:

#include "hello.h"
#include <iostream>

using namespace std;
using namespace hello;

int main(int argc, char const *argv[])
{
	//cout << hello::kValueCpp << endl;//编译错误
	//cout << hello::kStaticValueCpp << endl;//编译错误
	cout << "hello::kStaticValue:" << hello::kStaticValue << endl;
	cout << "hello::kValue:" << hello::kValue << endl;
	Hello h;
	h.print();
	return 0;
}

主函数直接访问cpp中的匿名命名空间变量,编译出错。

20.友元

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。
友元函数是可以直接访问类的私有成员非成员函数
友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样(不需要加点访问)。
它是定义在类外的普通函数,它不属于任何类。
示例代码:

#include "cmath"
#include "iostream"
using namespace std;
class Point
{
public:
      Point(double xx,double yy)
      {
          x=xx;
          y=yy;
      }
      void GetXY();
      friend double Distance(Point &a,Point &b);
protected:
private:
      double x,y;
};
void Point::GetXY()
{
     cout<<"("<<x<<","<<y<<")"<<endl;
}
double Distance(Point &a,Point &b)//友元函数不属于任何类,所以不需要加Point::
{
     double length;
     length=sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y));     //它可以引用类中的私有成员
     return length;
}
int main(void)
{
     Point p1(3.0,4.0),p2(6.0,8.0);
     p1.GetXY();    //成员函数的调用方法,通过使用对象来调用
     p2.GetXY();
     double d = Distance(p1,p2);     //友元函数的调用方法,同普通函数的调用一样,不要像成员函数那样调用
     cout<<d<<endl;
     return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值