C/C++
理解声明
变量声明由两部分组成,一个类型和一组具有特定格式的,期望用来对该类型求值的表达式。这种逻辑同样可以推广到函数、指针以及一些复杂的组合型式的声明。
float fn();
这个声明的含义是:表达式fn()求值结果是一个浮点数,也就是说,fn是一个返回值为浮点类型的函数。类似的
float *g(), (*h)();
表示*g()
与(*h)()
是浮点表达式。因为()结合优先级高于*
,*g()
也就是*(g())
g是一个函数,该函数的返回值类型为指向浮点数类型的指针。同理,可以得出h是一个函数指针,h所指向的函数的返回值为浮点类型。
一旦我们知道了如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:只要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可。 例如,因为下面的声明:
float (*h)();
表示h是一个指向返回值为浮点类型的函数指针,因此,
(float (*)())
就表示一个“指向返回值为浮点类型的函数的指针”的类型转换符。
(*(void(*)())0)()
,表示将常数0转型为“指向返回值为void的函数的指针”类型,然后再调用这个函数。
声明部分的作用是对有关的标识符(如变量、函数、结构体、共用体等)的属性进行说明。对于函数,声明和定义的区别是明显的,函数的声明是函数的原型,而函数的定义是函数功能的确立。
对变量而言,在声明部分出现的变量有两种情况:一种是需要建立存储空间的(如int a;);另一种是不需要建立存储空间的(如extern int a;)。前者称为定义性声明(defining declaration),或简称为定义(definition)。后者称为引用性声明(referenceing declaration)。广义地说,声明包括定义,但并非所有的声明都是定义。对“int a;” 而言,它是定义性声明,既可说是声明,又可说是定义。而对“extern int a;” 而言,它是声明而不是定义。
变量属性
一个变量除了数据类型以外,还有3种属性:
- 存储类别:C++允许使用auto,static,register和extern 4种存储类别。
- 作用域:指程序中可以引用该变量的区域。
- 存储期:指变量在内存的存储期限。
以上3种属性是有联系的,程序设计者只能声明变量的存储类别,通过存储类别可以确定变量的作用域和存储期。
要注意存储类别的用法。auto, static和register 3种存储类别只能用于变量的定义语句中,如:
- auto char c; //字符型自动变量,在函数内定义。函数中的变量不是static的一般就是自动变量
- static int a; //静态局部整型变量或静态外部整型变量
- register int d; //整型寄存器变量,在函数内定义
- extern int b; //声明一个已定义的外部整型变量
说明: extern只能用来声明已定义的外部变量,而不能用于变量的定义。只要看到extern,就可以判定这是变量声明,而不是定义变量的语句。
使用static声明的外部变量不能用于外部文件
static
class Static {
public:
static int i;
static int getI(){
return i;
}
};
int Static::i = 0; //必须初始化
- 通过类名访问静态成员需要使用域解析符“::”,也可以通过对象访问静态成员
sta.i
- 静态成员变量必须初始化,而且只能在类体外进行
指针
指针存储的值记录了指向的内存区的首地址,对于变量,数组,函数这些都会在内存分配空间,所以都能够使用指针指向他们,同时数组名、函数名本身就是一个地址,所以也就可以直接将数组名,函数名赋值给对应的指针变量。
指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。
指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。
int *ptr;
//指针的类型是int *
char *ptr;
//指针的类型是char *
int **ptr;
//指针的类型是int **
int (*ptr)[3];
//指针的类型是int(*)[3]
int *(*ptr)[4]; //指针的类型是
int ()[4]`int (*func)(void);
//指针的类型是int (*)()
指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*
去掉,剩下的就是指针所指向的类型。例如:
int *ptr;
//指针所指向的类型是int
char *ptr;
//指针所指向的类型是char
int **ptr;
//指针所指向的类型是int *
int (*ptr)[3];
//指针所指向的类型是int()[3]
int *(*ptr)[4]; //指针所指向的类型是
int *()[4]`int (*func)(void);
//指针所指向的类型是int ()()
在指针的算术运算中,指针所指向的类型有很大的作用。
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越来越熟悉时,你会发现,把与指针搅和在一起的“类型”这个概念分成“指针的类型”和“指针所指向的类型”两个概念,是精通指针的关键点之一。
指针的值
指针的值,或者叫指针所存储的内存地址编号。
指针的值是指针本身存储的值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。
指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为sizeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。
指针所指向的内存区和指针所指向的类型是两个完全不同的概念。
int * ptr;
在上面例子中,指针所指向的类型已经有了但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。
当我们声明一个指针变量的时候,如果没有给指针捆绑一个存储区,那指针会随机捆绑一个存储区,这样的做法很危险,会带来不可预料的后果。所以声明一个指针变量的时候必须给指针变量捆绑一个存储区,或者让它成为一个空指针(空指针里面记录记录空地址(NULL),这个地址数值就是0)。
指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。
指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。
指针的运算
地址数据可以进行如下计算
地址 + 整数
地址 - 整数
地址 - 地址
地址数据加减整数n事实上加减的是n个捆绑地址的数据类型的大小(也就是 sizeof(指针所指向的类型)的大小)。地址之间做减法结果是一个整数,这个整数代表两个地址之间包含的捆绑存储区个数(sizeof(指针所指向的类型))。
上面的运算对于数组和指针都是可行的,对于指针还可以自增(++
),自减(--
)
访问数组的元素
先回忆一下一些简单的概念
两个操作符
*
取指针指向的数据类型的值&
取变量的地址
引用一个数组元素可以用:
- 下标法:即用a[i]形式访问数组元素。
- 指针法:即采用(a+i)或(p+i)形式,用间接访问的方法来访问数组元素,其中a是数组名,p是指针变量,其初值p=a。
声明一个指针时使用了几个*
或[]
,访问一个数组元素时同样需要使用相同个数的*
或[]
才能访问到相应元素
下面是些例子
int n[][3] = {10,20,30,40,50,60};
int (*p)[3];
p = n;
cout << p[0][0] << endl << *(p[0]+1) << endl << (*p)[2] <<endl << *(*(p+1)+1) << endl;
int m[3] = {1,2,};
int * pm = m;
cout << *(pm+2) << endl;
n是一个二维数组,p是一个指向数组的指针,这个数组是个一维数组,有三个元素。
p=n, 也就是将二维数组的第一维的第一个元素的地址赋给了p,相当于p=&n[0]
这里p和n在访问数组元素上是等效的。因此p[0][0]不难理解
*(p[0]+1)
,p[0]就是一个地址,这个地址是第二维数组元素的首地址,如果理解为一个不可变的指针,那么指针类型是int *
,指向的数据类型是int,对其加1,也就是&p[0][0] + 1,即指向下一个元素的地址 &p[0][1]
(*p)[2]
,(*p)
取指针指向的数据类型的值,p的数组类型是一个三个元素的一维数组,也就是 (&n[0]),即 n[0],`(p)[2]`也就是n[0][2]
*(*(p+1)+1)
,(p+1),p加1,地址加的是sizeof(一维的三个元素的整型数组)的大小,也就是 &n[0] + 1,即&n[1],*(p+1)
即为n[1]。*(p+1)+1
就是 n[1]+1 即 &n[1][1],最后*(*(p+1)+1)
就是n[1][1]了。通过这个可以看出使用指针要小心,你甚至可以*(*(p+2)+1)
但是这个空间是未知的。
易混概念
指针函数和函数指针
Type *func(int a);
Type (*func)(int a);
第一个声明一个函数,这个函数返回一个指针类型,是指针函数,第二个声明一个函数指针,func是一个指针,指向一个函数的内存地址。
void add(int a);
typedef void (*FuncT)(int a);
FuncT fp = add; //FuncT fp = &add; 也是可以的,甚至可以这样调用(*add)(); 函数名就是函数的地址
(*fp)(1);
typedef定义一个函数指针类型,然后声明一个函数指针fp指向函数add的地址。
指针数组和数组指针
Type *arrP[8];
Type (*arrP)[8];
第一个声明一个数组,数组的元素是指针,即指针数组,第二个声明一个指针,指针指向一个数组,如上指向一个含有八个元素的数组
int (*p)[4];
int a[2][4] = {{1,2,3,4},{5,6,7,8}};
p = &a[0];
for(int i = 0;i<8;i++){
printf("%d ",(*p)[i]);
}
//1 2 3 4 5 6 7 8
函数模板和模板函数
template <模板类型参数> <返回值类型> <函数名> (模板函数形参表) {//函数体}
上面是一个函数模板,它是一组函数的描述,并不能直接执行,需要实例化为模板函数之后才能执行。
template<typename T>
void min(T &x, T &y) {
return (x<y)?x:y;
}
min(1, 2);
编译器按最先遇到的实参的类型隐含地生成一个模板函数
类模板和模板类
与函数模板和模板函数类似
#include<iostream>
template<class T>
class A {
public:
A(T value) {
this->value=value;
}
T getValue() {
return value;
}
private:
T value;
};
void main() {
A<int> a(2);
std::cout<<a.getValue()<<std::endl;
}
指针常量和常量指针
Type * const p1;
Type const * p2;
从右向左读的记忆方式:
- p1 is a const pointer to Type. 故p1不能指向别的字符串,但可以修改其指向的字符串的内容
- p2 is a pointer to const Type. 故
*p2
的内容不可以改变,但p2可以指向别的字符串
class 和 struct 区别
用class和struct关键字定义类的唯一差别在于默认访问权限:默认情况下,struct的成员为public(因此可以省略public关键字),而class的成员为private。
class还可以用于定义模板参数,如template <class Type>
或template <typename Type>
,不能写template <struct Type>
可以直接定义struct变量,也可以直接定义class变量。
struct person{
int age;
}per;
class person{
int age;
}per;
C和C++的struct区别
- C 的 struct 没有方法,而 C++的 struct 可以有方法
- C 的 struct 中成员没有访问控制权限, C++的 struct 则有,且默认 public
- C 的 struct 不能为空,而C++的 struct 则可以为空,大小为 1(空类大小也是1)。
内存
全局变量、静态数据、常量存放在全局数据区,所有类成员函数和非成员代码存放在代码区,为运行函数而分配的局部变量、函数参数、返回数据、返回地址等存放在栈区,余下的空间作为堆区(new/delete,malloc/free)
sizeof获取占用内存空间的大小
字节对齐
默认32位按4字节对齐,64位按8字节对齐。也可以指定
#pragma pack(n) //按n字节对齐
#pragma pack() //按默认对齐方式
结构体的大小
字节对齐的细节和编译器的实现相关,但对结构体一般而言,满足三个准则:
- 结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
- 结构体的每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要,编译器会在成员之间加上填充字节(internal adding)。
- 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员后加上填充字节(trailing padding)。
注意:空结构体(不含数据成员)的sizeof值为1。试想一个“不占空间“的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢,于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。
struct s1 {
short d;
char c;
char c1;
char c2;
int a;
double b;
};
sizeof(struct s1); //24
联合体的大小
联合体(union)按最宽的成员计算大小
数组的大小
可以根据数组类型和数组长度计算数组的空间大小。
需要注意的是,作为函数参数时数组会退化为指针。
指针与引用
指针是一个实体(本质上就是存放变量地址的一个变量),而引用只是个别名。 这句话从内存分配的角度很好理解,程序会为指针变量分配内存区域,而引用不分配内存区域。
指针可变,引用不可变。 指针在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。 引用在逻辑上不独立,具有依附性,所以引用必须在一开始就被初始化,而且引用一旦和某个对象绑定后就不能再改变(从一而终)。
指针可以为空,引用不能为空。 即指针可以为NULL,而引用必须与合法的存储单元关联。
传递方式不同。 首先,函数参数和返回值的传递方式大概可以理解为三种: 值传递 指针传递 引用传递
先理解值传递:值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个拷贝。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。
指针传递本质上值传递,只不过它所传递的是一个地址值。然后把上面那段话「翻译」一下:指针传递时,形参是一个指针变量,该变量拷贝了实参的地址值,然后作为被调函数的局部变量(传递过来的实参的地址值不会变),再然后我们可以用*操作符访问实参,从而对实参进行操作。很绕口
而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址(不必通过*操作符),即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量(此时的形参其实就是实参)。
程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。
引用不能const,指针能const,const的指针不可变。 对于引用,具体指没有int&const a这种形式,而const int& a是有的, 前者指引用本身(即别名)不可以改变,这是理所当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
- sizeof(引用) 得到的是所指向的变量(对象)的大小,而sizeof(指针)得到的是指针本身的大小。
- 指针和引用的自增(++)运算意义不一样(很好理解)。
const
为什么使用const?采用符号常量写出的代码更容易维护;指针常常是边读边移动,而不是边写边移动;许多函数参数是只读不写的。const最常见用途是作为数组的大小和switch分情况标号(也可以用枚举符代替),分类如下:
- 常变量:
const 类型说明符 变量名
- 常引用:
const 类型说明符 &引用名
- 常对象:
类名 const 对象名
- 常成员函数:
类名::fun(形参) const
- 常数组:
类型说明符 const 数组名[大小]
- 常指针:
const 类型说明符* 指针名 ,类型说明符* const 指针名
首先提示的是:在常变量(const 类型说明符 变量名)、常引用(const 类型说明符 &引用名)、常对象(类名 const 对象名)、 常数组(类型说明符 const 数组名[大小]), const” 与 “类型说明符”或“类名”(其实类名是一种自定义的类型说明符) 的位置可以互换。
常量
声明时必须进行初始化(C++类中则不然)。const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。―《C++ Program Language》
用const声明的变量虽然增加了分配空间,但是可以保证类型安全。
C标准中,const定义的常量是全局的,C++中视声明位置而定。
指针和常量
使用指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用const“预先固定”将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,必须使用声明运算符*const
。
所以出现在 *
之前的const是作为基础类型的一部分:
char *const cp;
到char的const指针char const *pc1;
到const char的指针const char *pc2;
到const char的指针(后两个声明是等同的)
对于常量指针和指针常量要区分究竟是什么不能变
A * const b = new A();
对象内容可变,指针指向不可变const A * c = new A();
指针指向可变,对象内容不可变const A * const d = new A();
指针指向不可变,对象内容不可变
注意:允许把非 const 对象的地址赋给指向 const 对象的指针,不允许把一个 const 对象的地址赋给一个普通的、非 const 对象的指针。
const char c[] ="boy"; //定义 const 型的 char 数组
const char * pi; //定义pi为指向const型的char变量的指针变量
pi = c; //合法,pi指向常变量(char数组的首元素)
char *p2=c; //不合法,p2不是指向常变量的指针变量
如果定义了一个指向常对象的指针变量,并使它指向一个非const的对象,则其指向的对象是不能通过指针来改变的。如:
Time t1(10,12,15); //定义Time类对象t1,它是非const型对象
const Time *p = &t1; //定义p是指向常对象的指针变量,并指向t1
t1.hour = 18; //合法,t1不是常变量
(* p).hour = 18; //非法,不齙通过指针变量改变t1的值
const修饰函数传入参数
将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
通常修饰指针参数和引用参数:
void Fun( const A *in); //修饰指针型传入参数
void Fun(const A &in); //修饰引用型传入参数
修饰函数返回值
可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。
const修饰成员函数(C++特性)
- const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
- const对象的成员是不能修改的,而通过指针维护的对象却是可以修改的;
- const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。除非数据成员有mutable关键字修饰,这种情况下,被mutable修饰的成员变量可以被const成员函数修改。
class A {
void f(int i) {...} //一个函数
void f(int i) const {...} //上一个函数的重载,相当于void f(A const* this,int i) {...} 防止数据成员被改变
};
A const a;
a.f(1); //调用第二个f函数
常引用
关于引用的初始化有两点值得注意:
- 当初始化值是一个左值(可以取得地址)时,没有任何问题;
- 当初始化值不是一个左值时,则只能对一个const T&(常量引用)赋值。而且这个赋值是有一个过程的:
首先将值隐式转换到类型T,然后将这个转换结果存放在一个临时对象里,最后用这个临时对象来初始化这个引用变量。在这种情况下,const T&(常量引用)过程中使用的临时对象会和const T&(常量引用)共存亡。
例子:
double& dr = 1; // 错误:需要左值
const double& cdr = 1; // ok
第二句实际的过程如下:
double temp = double(1);
const double& cdr = temp;
函数调用时
void bar(string &s);
bar("hello world");
//上面方法调用是错误的
//如果函数声明如下就可以
void bar(const string &s);
左值、右值
左值就是可以出现在表达式左边的值(等号左边),可以被改变,它是存储数据值的那块内存地址,也称为变量的地址;右值是指存储在某内存地址中的数据,也称为变量的数据。左值可以作为右值,但右值不能做左值。
++a表示取i的地址,对其内容加1,++a代表的还是内存地址,可以当左值使用。a++表示取a的地址,把它的值装入寄存器,然后对内存中的a的值进行加1,表达式a++只是表示一个拷贝,不可以做左值。
++i += i; //正确
i++ += i; //错误
explicit
class String{
String(const char* p){}
};
String s = "hello";
上述赋值过程将会发生隐式类型转换String("hello"),C++中一个参数的构造函数有两个功能:构造器和默认且隐含的类型转换操作符。
class String{
explicit String(const char* p){}
};
String s = "hello";
添加explicit关键字后,上面的赋值语句将会错误。可以如下使用
String s("hello");
宏
可以用#define命令将一个指定的标识符(即宏名)来代表一个字符串。定义宏的作用一般是用一个短的名字代表一个长的字符串。它的一般形式为:
#define 标识符 字符串
这就是已经介绍过的定义符号常量。如:
#define PI 3.1415926
还可以用#define命令定义带参数的宏定义。其定义的一般形式为:
#define 宏名(参数表) 字符串
如:
#define S(a, b) a*b //定义宏S(矩形面积),a,b为宏的参数
使用的形式如下:
area=S(3, 2);
注意枚举、typedef、内联函数、const和#define
的区别
宏定义就是在预编译时做一个替换,没有类型检查,不利于调试
枚举是有类型的,属于常量
typedef有类型检查的功能,有作用域限制,可以用来定义类型别名,#define
则只是替换字符串,和#define
相比对指针的操作不同
内联函数在编译时在其调用处,会将函数的内容展开,有类型检查,关键字是inline,宏定义只是进行代码替换
const 常量有数据类型,会分配实实在在的空间
枚举
枚举元素作为常量,它们是有值的,C++编译按定义时的顺序对它们赋值为0,1,2,3,…。也可以在声明枚举类型时另行指定枚举元素的值。
enum number {
one = 1,
two,
zero = 0,
three = 3,
four
};
printf("%d %d %d %d %d", zero, one, two, three, four);
typedef
声明一个新的类型名的方法是:
- 先按定义变量的方法写出定义语句(如int i;)。
- 将变量名换成新类型名(如将i换成COUNT)。
- 在最前面加typedef(如typedef int COUNT)。
- 然后可以用新类型名去定义变量
typedef int NUM[100]; //声明NUM为整型数组类型,包含100个元素
NUM n; //定义n为包含100个整型元素的数组
typedef char *STRING; //声明STRING为字符指针类型
STRING p,s[10]; //p为字符指针变量,s为指针数组(有10个元素)
typedef int (*POINTER)( ) //声明POINTER为指向函数的指针类型,函数返回整型值
POINTER p1, p2; // p1,p2为POINTER类型的指针变量
结构体位段
typedef struct {
int a:2;
int b:2;
int c:1;
}test;
int main(){
test t;
t.a = 1;
t.b = 3;
t.c = 1;
printf("%d\n%d\n%d\n",t.a,t.b,t.c); //1 -1 -1
printf("%d\n",sizeof(t)); //4
}
定义位段的长度不能大于存储单元的长度(存储单元指该位段的类型大小);如a占两位,被赋值为1,二进制就是01,b占两位,被赋值为3,二进制是11,也就是-1;a占两位,a原有存储空间是32位,剩余空间,会存储下个位段,也就是b,最后这个结构体就是4个字节
位运算
计算一个数乘以7
x<<3 -x
位运算计算平均值
(x&y)+((x^y)>>1)
验证 计算两个2147483647的平均值,正常除2会溢出,这种方法可以求得正确结果。
x & y
,求两个数的二进制下的相同位的和的一半:eg : 1 & 1 = 1; 0 & 0 = 0; 可以看出的确是一半x ^ y
,求两个数的二进制下的不同位的和:eg: 1 ^ 0 = 1>> 1
,除以2- 所以整个表达式的结果就是将(x+y)/2
求绝对值
y = x>>31;(x^y)-y
或(x+y)^y;
无符号数
unsigned int i = 3;
printf("%u\n", i*-1); //424967293
判断操作系统多少位
还是利用无符号数
unsigned int a = ~0;
if(a > 65536)
printf("32"); //32位
else
printf("16"); //16位
判断大小端
int checkCPU(){
union w {
int a;
char b;
}c;
c.a=1;
return (c.b == 1); //大端
}
常用
- 常用等式:-n = ~(n - 1) = ~n + 1
- 获取整数n的二进制中最后一个1:n&-n,或n&~(n-1)
- 去掉整数n的二进制中最后一个1:n&(n-1)
函数的重载、覆盖、隐藏:
- 重载:成员函数具有以下特征时发生“重载”
- 在同一个类中
- 函数名相同
- 参数的类型、个数顺序不同(不能进行隐式转换)
- virtual可有可无
- 与函数的返回类型无关
- 覆盖:指子类函数覆盖父类函数,其特征是:
- 不在同一个类中(分别位于子类与父类中)
- 函数名相同
- 父类函数必须有virtual
- 参数相同
- 返回值类型也相同
- 隐藏:子类函数屏蔽了父类的函数,规则如下:
- 如果子类函数与父类函数同名,但返回值或参数不同,此时不论父类函数有没有virtual,子类函数都将隐藏父类函数
- 如果子类函数与父类函数同名,返回值和参数相同,父类函数没有virtual,则父类函数将被隐藏
数组
数组初始化
int a[2][3] = {{1},{2}};
int a[2][3] = {1,2};
二维数组可以使用上面两种方式初始化,没有显示初始化的元素,默认为0,第一个除了a[0][0]和a[1][0]有非零值,其他均为0,第二个数组,除了a[0][0]和a[0][1]有非零值,其他均为0。
int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};
对第一维的长度可以不指定,编译器会推算出第一维的长度。
数组首地址和数组元素首地址
&a
表示数组首地址,而a
或&a[0]
表示数组元素首地址,如果指针p被赋值为&a
,p+1
表示以sizeof(Type)为移动单位,向下一位置移动。这里p指向这个数组的首地址,向前移动一步,sizeof(Type)是数组大小,而如果指针p被赋值为a
,p+1
则表示向前移动一个数组元素大小的长度。
int a[5] = {1,2,3,4,5};
int *p = (int*)(&a+1);
printf("%d %d\n", *(a+1), *(ptr-1)); //2 5
printf("%d\n", sizeof(a)); //20
printf("%d\n", sizeof(&a)); //20
构造函数和析构函数
构造函数可以重载,析构函数不可以
析构函数可以为虚函数,构造函数不可以,而且最好把析构函数声明为虚函数,能够释放掉子类的资源。
都没有返回值,析构函数不能有参数
构造函数可以抛出异常,析构函数不能抛出异常。然而,在构造函数中抛出异常的时候,不会去调用析构函数,此时如果处理不当,会出现内存泄露。
析构函数的作用是释放资源,如果某一行代码抛出了异常,后面的代码将得不到执行,造成内存泄露。
初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。例如:
class A{
public:
int i;
A(int i){
this->i = i;
}
};
class CExample {
public:
float b;
A a;
const int i;
//构造函数初始化列表
CExample(): a(1), b(8.8), i(2)
{}
//构造函数内部赋值,构造函数初始化列表
CExample(float b): a(1), i(2)
{
this->b=b;
}
};
class B:public A{
public:
B():A(1){
}
};
上面的例子中两个构造函数的结果是一样的。上面的构造函数(使用初始化列表的构造函数)显式的初始化类的成员;而没使用初始化列表的构造函数是对类的成员赋值,并没有进行显式的初始化。
初始化和赋值对内置类型的成员没有什么大的区别,像上面的任一个构造函数都可以。对非内置类型成员变量,为了避免两次构造,推荐使用类构造函数初始化列表。但有的时候必须用带有初始化列表的构造函数:
- 成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化)
- const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。
初始化的顺序和成员在类中声明的顺序一致。
构造函数调用顺序
先执行虚拟继承的父类的构造函数,然后从左到右执行普通继承的父类的构造函数,然后按照定义的顺序执行数据成员的初始化,最后是自身的构造函数的调用。析构函数与之完全相反,互成镜像。
访问规则
基类中的成员 | 在公用派生类中的访问属性 | 在私有派生类中的访问属性 | 在保护派生类中的访问属性 |
---|---|---|---|
私有成员 | 不可访问 | 不可访问 | 不可访问 |
公用成员 | 公用 | 私有 | 保护 |
保护成员 | 保护 | 私有 | 保护 |
virtual
虚基类
类A派生出类B和类C,类D继承自类B和类C,这个时候类A中的成员变量和成员函数继承到类D中变成了两份,一份来自 A-->B-->D 这一路,另一份来自 A-->C-->D 这一条路。
C c;
c.A.a; //访问的是A类的成员a
c.B.a; //访问的是B类的成员a
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突,而且很少有这样的需求。
为了解决这个问题,C++提供了虚基类,使得在派生类中只保留间接基类的一份成员。
声明虚基类只需要在继承方式前面加上 virtual 关键字
class A { ... };
class B : virtual public A { ... };
class C : virtual public A { ... };
class D : public B, public C { ... };
在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。
虚函数
作为C++中实现多态的重要方式,虚函数在被调用的时候,具体执行的代码必须能够找到对应的对象的动态类型,从而调用正确的函数。为了做到这一点,大部分编译器都是通过虚函数表(vtbl)和虚函数表指针(vptr)来实现的。
对于每一个包含虚函数的类都需要专门的空间存放这个类的虚函数表。需要在包含虚函数的类的每个对象中放置一个额外的指针,这个指针指向虚函数表。因此,当父类指针指向子类的实例时,仍能够通过虚函数表调用子类的方法。
class A{
public:
A(){}
virtual void foo(){
printf("A foo");
}
};
class B:public A{
public:
B(){}
virtual void foo(){
printf("B foo");
}
};
A *a = new B();
a->foo(); //B foo
定义虚函数的限制:
- 非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
- 只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。
- 当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数(函数名相同、参数列表完全一致、返回值类型相关)自动成为虚函数。
- 如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。
不能被声明为虚函数的函数
- 顶层函数:多态的运行期行为体现在虚函数上,虚函数通过继承方式来体现出多态作用,顶层函数不属于成员函数,是不能被继承的。
- 构造函数:1)构造函数不能被继承,因而不能声明为virtual函数。2)构造函数一般是用来初始化对象,只有在一个对象生成之后,才能发挥多态的作用,如果将构造函数声明为virtual函数,则表现为在对象还没有生成的情况下就使用了多态机制,因而是行不通的
- 静态函数:只属于某个类。
- 友元函数:友元函数不属于类的成员函数,不能被继承。
- 内联函数:inline函数和virtual函数有着本质的区别,inline函数是在程序被编译时就展开,在函数调用处用整个函数体去替换,而virtual函数是在运行期才能够确定如何去调用的,因而inline函数体现的是一种编译期机制,virtual函数体现的是一种运行期机制。此外,一切virtual函数都不可能是inline函数。
Base b;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
纯虚函数
- 当在基类中不能为虚函数给出一个有意义的实现时,可以将其声明为纯虚函数,其实现留待派生类完成。
- 纯虚函数的作用是为派生类提供一个一致的接口。
- 有纯虚函数的类不能实例化,但可以声明指针。
需要注意后面的class A{ public: virtual void foo()=0; };
=0
不能少。
凡是包含纯虚函数的类都是抽象类。因为纯虚函数是不能被调用的,包含纯虚函数的类是无法建立对象的。
纯接口类:
- 仅有纯虚函数(public pure virtual)和静态函数(static method)(Java不能有静态方法)
- 不存在非静态数据成员
- 不必定义构造函数。如果有的话,则必须是protected,且不接受任何参数。
- 如果本身是一个子类,那么其父类也要满足上述条件并以Interface作为后缀。
多态
面向对象的多态性可以分为四类:重载多态、强制多态、引用多态和参数多态.
- 普通函数及类的成员函数的重载,运算符重载属于重载多态
- 强制多态:将一个变元的类型加以变化,即类型强制转换
- 引用多态:虚函数
- 参数多态:类模板.
拷贝构造函数
C++ 空类,默认产生哪些成员函数。
默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符 这四个是我们通常大都知道的。但是除了这四个,还有两个,那就是取址运算符和 取址运算符 const
即总共有六个函数。
class Empty {
public:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};
但是,C++默认生成的函数,只有在被需要的时候,才会产生。即当我们定义一个类,而不创建类的对象时,就不会创建类的构造函数、析构函数等。
1) 对于一个类X,如果一个构造函数的第一个参数是下列之一
a) X& b) const X& c) volatile X& d) const volatile X&
且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数。
X::X(const X&); //是拷贝构造函数
X::X(X&, int=1); //是拷贝构造函数
2) 类中可以存在超过一个拷贝构造函数 3) 拷贝构造函数不能由成员函数模版生成
struct X {
template<typename T>
X( const T& ); // NOT copy ctor, T can't be X
template<typename T>
operator=( const T& ); // NOT copy ass't, T can't be X
};
拷贝构造函数的调用时机
- 一个对象以值传递的方式传入函数体
- 一个对象以值传递的方式从函数返回
- 一个对象需要通过另外一个对象进行初始化。
什么时候调用拷贝构造函数什么时候调用赋值函数,看下面的例子
String a("hello");
String b("world");
String c = a; // 调用了拷贝构造函数,最好写成c(a);
c = b; // 调用了赋值函数
另外这些函数,只有在没有时,才会默认生成。而如果提供了声明,即使没有实现,也不会生成默认的函数。因此我们通常用的方法是将拷贝构造和赋值设置为private而不实现,以禁止类的拷贝和赋值。如果有很多类都有这种需求,那么可以定义一个基类,然后让其他类继承这个类。
浅拷贝和深拷贝
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝
自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
内联函数
inline 关键字放在函数声明处不会起作用。inline 关键字应该与函数体放在一起:
void swap(int &a, int &b);
inline void swap(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
在类体中和类体外定义成员函数是有区别的:在类体中定义的成员函数为内联(inline)函数,在类体外定义的不是。
内联函数一般不是我们所期望的,它会将函数调用处用函数体替代,所以我建议在类体内部对成员函数作声明,而在类体外部进行定义,这是一种良好的编程习惯。
当然,如果你的函数比较短小,希望定义为内联函数,那也没有什么不妥的。
如果你既希望将函数定义在类体外部,又希望它是内联函数,如下所示:
class Student{
public:
char *name;
int age;
float score;
public:
void say();
};
inline void Student::say(){
printf("%s的年龄是 %d,成绩是 %f\n", name, age, score);
}
请特别注意,类声明和函数定义一般是分别放在两个文本中的。由于要求接口与实现分离,为软件开发商向用户提供类库创造了很好的条件。
友元
友元提供了一种 普通函数或者类成员函数 访问另一个类中的私有或保护成员 的机制。也就是说有两种形式的友元:
- 友元函数:普通函数对一个访问某个类中的私有或保护成员。
- 友元类:类A中的成员函数访问类B中的私有或保护成员。
特性
优点:提高了程序的运行效率。 缺点:破坏了类的封装性和数据的透明性。
友元函数
在类声明的任何区域中声明,而定义则在类的外部。
friend <类型><友元函数名>(<参数表>);
注意,友元函数只是一个普通函数,并不是该类的类成员函数,友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。它可以在任何地方调用,友元函数中通过对象名来访问该类的私有或保护成员。
// TestFriend.cpp
#include "stdafx.h"
class A {
public:
A(int _a):a(_a){};
friend int getA_a(A &_classA);//友元函数
private:
int a;
};
int getA_a(A &_classA) {
return _classA.a;//通过对象名访问私有变量
}
int main(int argc, _TCHAR* argv[]) {
A _classA(3);
std::cout<<getA_a(_classA);//友元函数只是普通函数,可以在任意地方调用
return 0;
}
友元类
友元类的声明在该类的声明中,而实现在该类外。
friend class <友元类名>;
友元类的实例则在main函数中定义。
// TestFriend.cpp
#include "stdafx.h"
class B {
public:
B(int _b):b(_b){};
friend class C;//声明友元类C
private:
int b;
};
class C//实现友元类C
{
public:
int getB_b(B _classB){
return _classB.b;//访问友元类B的私有成员
};
};
int main(int argc, _TCHAR* argv[]) {
B _classB(3);
C _classC;//定义友元类实例
std::cout<<_classC.getB_b(_classB);
return 0;
}
注意
- 友元关系没有继承性 假如类B是类A的友元,类C继承于类A,那么友元类B是没办法直接访问类C的私有或保护成员。
- 友元关系没有传递性 加入类B是类A的友元,类C是类B的友元,那么友元类C是没办法直接访问类A的私有或保护成员,也就是不存在“友元的友元”这种关系。
友元函数不能访问静态成员。
C++中的不能被继承的类
class FinalClass{
public:
static FinalClass * getInstance(){
return new FinalClass;
}
static void deleteInstance(FinalClass instance){
delete instance;
instance = NULL;
}
private:
FinalClass(){}
~FinalClass(){}
};
通过上面的方法可以使FinalClass不能被继承,但是该方法得到的实例都位于堆上,需要程序员手动释放。
template <typename T> class MakeFinal {
friend T;
private:
MakeFinal(){}
~MakeFinal(){}
};
class FinalClass: virtual public MakeFinal<FinalClass>{
public:
FinalClass(){}
~FinalClass(){}
};
class Try: public FinalClass{
public:
Try(){}
~Try(){}
};
Try temp;
由于类FinalClass是从类 MakeFinal<FinalClass>
虚继承过来的,在调用Try的构造函数时,会直接跳过FinalClass,而直接调用MakeFinal<FinalClass>
的构造函数。但由于类Try不是MakeFinal<FinalClass>
的友元,因此不能调用其私有的构造函数。所以,试图从FinalClass继承的类,一旦实例化,都会导致编译错误,因此FinalClass不能被继承。
malloc/free 与 new/delete 的区别点
区别点:
- new/delete 是 C++运算符,而 malloc/free 是标准库函数,用之前要包含其头文件
<malloc.h>
- new 在分配空间时会自动调用类的构造函数, malloc 只管分配空间,不会调用构造函数
- delete 在释放空间时会调用类的析构函数,而 free 则不会调用析构函数
- new/delete 是可以重载的,而重载之后,就成为了函数。当 new/delete 在类中被重载的时候,可以自定义申请过程,比如记录所申请内存的总长度,以及跟踪每个对象的指针。
- malloc 在申请内存的时候,必须要提供申请的长度,而且返回的指针是 void*型,必须要强转成需要的类型。 free 的返回值为空。
- new/delete,其实内部也调用了 malloc/free。
int *a = new int[3];
int (*c)[2] = new int[3][2];
int (*e)[2][3] = new int[3][2][3];
delete a;
delete c;
delete e;
malloc & calloc & realloc
- malloc 分配指定字节数的存储区。此存储区中的初始值不确定
- calloc 为指定长度的对象,分配能容纳其指定个数的存储空间。该空间中的每一位(bit)都初始化为0
- realloc 更改以前分配区的长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定
默认参数
可以在声明或者定义时直接对参数赋值,默认参数后不能出现非默认参数。
void fn(int i = 1);
代码膨胀
模板和内联函数会导致代码膨胀
模板实例化会有多个版本的函数,内联函数直接在调用处展开
运算符重载函数
运算符重载的函数名是由关键字operator和要重载的运算符组成。其形式:
返回类型 operator 运算符号 (参数列表)
不能重载的运算符只有5个:
.
(成员访问运算符)
.*
(成员指针访问运算符)
::
(域运算符)
sizeof
(长度运算符)
?:
(条件运算符)
对象数组
对象数组初始化
当有一个参数的构造函数时,比如有A(int i):year(i){}
A a[3] = {1,2,3};
当有多个参数的构造函数时,比如有A(int i,int j):year(i),month(j){}
A a[3] = {A(1,2),A(3,4),A(5,6)};
容器类
顺序容器
顺序容器:将单一类型元素聚集起来成为容器,然后根据位置来存储和访问这些元素。主要有vector、list、deque(双端队列)。顺序容器适配器:stack、queue和priority_queue。
- 迭代器
- c.begin() 返回一个迭代器,它指向容器 c 的第一个元素
- c.end() 返回一个迭代器,它指向容器 c 的最后一个元素的下一位置
- c.rbegin() 返回一个逆序迭代器,它指向容器 c 的最后一个元素
- c.rend() 返回一个逆序迭代器,它指向容器 c 的第一个元素前面的位置
- 添加元素
- c.push_back(t) 在容器c的尾部添加值为t的元素。返回void 类型
- c.push_front(t) 在容器c的前端添加值为t的元素。返回void 类型 只适用于list和deque容器类型。
- c.insert(p,t) 在迭代器p所指向的元素前面插入值为t的新元素。返回指向新添加元素的迭代器。
- c.insert(p,n,t) 在迭代器p所指向的元素前面插入n个值为t的新元素。返回void 类型
- c.insert(p,b,e) 在迭代器p所指向的元素前面插入由迭代器b和e标记的范围内的元素。返回 void 类型
- 容器大小的操作
- c.size() 返回容器c中元素个数。返回类型为 c::size_type
- c.max_size() 返回容器c可容纳的最多元素个数,返回类型为c::size_type
- c.empty() 返回标记容器大小是否为0的布尔值
- c.resize(n) 调整容器c的长度大小,使其能容纳n个元素,如果n<c.size(),则删除多出来的元素;否则,添加采用值初始化的新元素
- c.resize(n,t) 调整容器c的长度大小,使其能容纳n个元素。所有新添加的元素值都为t
- 访问元素
- c.back() 返回容器 c 的最后一个元素的引用。如果 c 为空,则该操作未定义
- c.front() 返回容器 c 的第一个元素的引用。如果 c 为空,则该操作未定义
- c[n] 返回下标为 n 的元素的引用。如果 n <0 或="" n="">= c.size(),则该操作未定义 只适用于 vector 和 deque 容器0>
- c.at(n) 返回下标为 n 的元素的引用。如果下标越界,则该操作未定义 只适用于 vector 和 deque 容器
- 删除元素
- c.erase(p) 删除迭代器p所指向的元素。返回一个迭代器,它指向被删除元素后面的元素。如果p指向容器内的最后一个元素,则返回的迭代器指向容器超出末端的下一位置。如果p本身就是指向超出末端的下一位置的迭代器,则该函数未定义
- c.erase(b,e) 删除迭代器b和e所标记的范围内所有的元素。返回一个迭代器,它指向被删除元素段后面的元素。如果e本身就是指向超出末端的下一位置的迭代器,则返回的迭代器也指向容器的超出末端的下一位置
- c.clear() 删除容器c内的所有元素。返回void
- c.pop_back() 删除容器c的最后一个元素。返回void。如果c为空容器,则该函数未定义
- c.pop_front() 删除容器c的第一个元素。返回void。如果c为空容器,则该函数未定义 只适用于 list 或 deque 容器
- 赋值与swap
- c1 = c2 删除容器c1的所有元素,然后将c2的元素复制给c1。c1和c2的类型(包括容器类型和元素类型)必须相同
- c1.swap(c2) 交换内容:调用完该函数后,c1中存放的是 c2 原来的元素,c2中存放的则是c1原来的元素。c1和c2的类型必须相同。该函数的执行速度通常要比将c2复制到c1的操作快
- c.assign(b,e) 重新设置c的元素:将迭代器b和e标记的范围内所有的元素复制到c中。b和e必须不是指向c中元素的迭代器
- c.assign(n,t) 将容器c重新设置为存储n个值为t的元素
- 容器容量
- capacity():获取在容器需要分配更多的存储空间之前能够存储的元素总数
- reserve():告诉vector容器应该预留多少个元素的存储空间
适配器的操作
栈适配器:
- s.empty() 如果栈为空,则返回true,否则返回stack
- s.size() 返回栈中元素的个数
- s.pop() 删除栈顶元素的值,但不返回其值
- s.top() 返回栈顶元素的值,但不删除该元素
- s.push(item) 在栈顶压入新元素
队列和优先级队列:
- q.empty() 如果队列为空,则返回true,否则返回false
- q.size() 返回队列中元素的个数
- q.pop() 删除队首元素,但不返回其值
- q.front() 返回队首元素的值,但不删除该元素 该操作只适用于队列
- q.back() 返回队尾元素的值,但不删除该元素 该操作只适用于队列
- q.top() 返回具有最高优先级的元素值,但不删除该元素 该操作只适用于优先级队列
- q.push(item) 对于queue,在队尾压入一个新元素,对于priority_quue,在基于优先级的适当位置插入新元素
关联容器
关联容器:支持通过键来高效地查找和读取元素。主要有:pair、set、map、multiset和multimap。
- 元素的访问
- m.at(index) (C++11) 访问指定的元素,同时进行越界检查
- m[index] 访问指定的元素
- 迭代器
- m.begin() 返回一个迭代器,它指向容器 m 的第一个元素
- m.end() 返回一个迭代器,它指向容器 m 的最后一个元素的下一位置
- m.rbegin() 返回一个逆序迭代器,它指向容器 m 的最后一个元素
- m.rend() 返回一个逆序迭代器,它指向容器 m 的第一个元素前面的位置
- 容量
- m.size() 返回容器m中元素个数。返回类型为 m::size_type
- m.max_size() 返回容器m可容纳的最多元素个数,返回类型为m::size_type
- m.empty() 返回标记容器大小是否为0的布尔值
- 清空
- m.erase(k) 删除m中键为k的元素。返回size_type类型的值,表示删除的元素个数
- m.erase(p) 从m中删除迭代器p所指向的元素。p必须指向m中确实存在的元素,而且不能等于m.end()。返回void
- m.erase(b, e) 从m中删除一段范围内的元素,该范围由迭代器对b和e标记。b和e必须标记m中的一段有效范围:即b和e都必须指向m中的元素或最后一个元素的下一个位置。而且,b和e要么相等(此时删除的范围为空),要么b所指向的元素必须出在e所 指向的元素之前。返回 void 类型
- m.clear() 删除容器c内的所有元素。返回void
- 插入
- m.insert(e) e是一个用在m上的value_type 类型的值。如果键(e.first不在m中,则插入一个值为e.second 的新元素;如果该键在m中已存在,则保持m不变。该函数返回一个pair类型对象,包含指向键为e.first的元素的map迭代器,以及一个 bool 类型的对象,表示是否插入了该元素
- m.insert(beg,end) beg和end是标记元素范围的迭代器,其中的元素必须为m.value_type 类型的键-值对。对于该范围内的所有元素,如果它的键在 m 中不存在,则将该键及其关联的值插入到 m。返回 void 类型
- m.insert(iter,e) e是一个用在m上的 value_type 类型的值。如果键(e.first)不在m中,则创建新元素,并以迭代器iter为起点搜索新元素存储的位置。返回一个迭代器,指向m中具有给定键的元素
- 查找
- m.count(k) 返回 m 中 k 的出现次数
- m.find(k) 如果m容器中存在按k索引的元素,则返回指向该元素的迭代器。如果不存在,则返回超出末端迭代器。
STL容器实现
序列式容器
- vector-数组,元素不够时再重新分配内存,拷贝原来数组的元素到新分配的数组中。
- list-双链表。
- deque-双端队列的数据被表示为一个分段数组,容器中的元素分段存放在一个个大小固定的数组中,此外容器还需要维护一个存放这些数组首地址的索引数组。所以使用deque的复杂度要大于vector,尽量使用vector。
- stack-基于deque。
- queue-基于deque。
- heap-完全二叉树,使用最大堆排序,以数组(vector)的形式存放。
- priority_queue-基于heap。
- slist-单向链表。
关联式容器
- set,map,multiset,multimap-基于红黑树(RB-tree),一种加上了额外平衡条件的二叉搜索树。
- hash_map,hash_set,hash_multiset,hash_multimap-基于散列表。
初始化
对于静态存储区的变量,如全局变量和static变量,会默认初始化为对应的0值。
而对于一个对象,new 时,后面带括号和不带括号是有所区别的。
class A{public:int m;}
A * a= new A; //m随机
A * a= new A(); //m为0
更详细的解释可以看下面
Do the parentheses after the type name make a difference with new?
参考链接
- 从void0理解c语言中的函数声明
- 数据存储类型:auto/static/register/extern
- 指针(详解)
- C语言通过指针引用数组
- C++中sizeof用法
- C++指针与引用小结
- C/C++中const关键字详解
- 继承中构造函数和析构函数的调用顺序
- C++奇奇怪怪的题目之构造析构顺序
- C++ 对象的内存布局
- C++虚函数、多继承和虚基类学习心得
- C++ 空类,默认产生哪些成员函数
- C++中复制构造函数与重载赋值操作符总结
- C++拷贝构造函数(深拷贝,浅拷贝)
- inline内联函数(声明前加inline还是定义前加inline)
- C++内联成员函数问题
- C++友元函数和友元类
- C++中容器总结
- STL容器的实现原理
- C++入门教程
- C++基础
- C++基础总结