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就要两次内存访问,效率就低了。
注意:
- long占用4个字节,long long占用8个字节。
- string类型不同的编译器占用空间,即sizeof(string)不同。
- 指针类型数据也要占空间,32位系统占32位(4字节),64位系统占64位(8字节)。
- 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)
- sizeof(class)是这个class的一个实例的大小,因此只会计算类的实例的大小。
- 空类占1字节,空类也会被实例化,所以编译器会给空类隐含的添加一个字节。仅包含普通成员函数的类也是占1字节。
- 成员变量的占用大小与struct相同。
- 包含虚函数的类,会增加一个虚函数指针,即加一个指针大小。
- 构造函数,析构函数,普通成员函数不占空间。
- 静态成员变量和函数是属于整个类,静态数据成员被编译器放在程序的一个global data members中,但是它不影响实例的大小,即不占空间。
- 普通的继承,派生类的大小是派生类自身的大小,加上基类成员变量的大小。实际大小就是基类和子类的所有数据成员大小,再加上一个虚表指针(如果有虚函数),基类的虚表指针不能算。
- 多继承,派生类大小要加上所有基类的大小。
- 虚继承,派生类中会有一个指向虚基类表的指针,另外,对于虚继承中,派生类中存在一个或多个虚函数的时候,它本身就有一个虚表,指向自己的虚表。基类也会有自己的虚表指针,都要加上。
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的定义如下:
- Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
- 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;
}
632

被折叠的 条评论
为什么被折叠?



