这篇博客是我之前的一个礼拜复习总结的各种知识点,可能有些多,其中的一些观点是来自于《Effective C++》和《C++编程思想》,这两本书中的知识给了我很多启发,也让我懂得了许多不一样的知识点,我连带我的认识以及理解整理起来,希望会对你们有所帮助。
资源就是一旦被使用,将来必须要返还给系统。在c++中最常使用的资源就是动态分配内存(如果分配了内存却从来不归还它,会导致内存泄漏
其他的常见资源还有 文件描述器,互斥锁,图形界面中的字型和笔刷,数据库连接,以及网络 sockets
void f()
{
Investment* pInv = createInvestment();
....... //这里看起来没有任何问题,但是如果有一个return 语句在这里提前中断,控制流就无法接触到delete语句
delete pInv;
}
-2 类似的情况还可能发生在循环内,如果循环内有一个 continue或者 goto 语句导致过早退出
-3 还有一种情况就是可能在 ... 中可能抛出异常,
无论delete是如何被忽略掉的,我们泄漏的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源
为了确保对象返回的资源总是可以被释放掉,我们必须把资源放进对象内,当控制流离开函数 f()时,该对象的析构函数会自动的释放那些资源
void f()
{
std::auto_ptrpInv(createInvestment());
//调用factory函数
//一如以往地使用pInv
//经由 auto_ptr 的析构函数自动删除pInv
}
std::auto_ptr
pInv1(createInvestment()); //pInv1 指向返回物
std::auto_ptr pInv2(pInv1); //pInv2指向对象,pInv1 设为NULL
pInv1 = pInv2; //pInv1 指向对象,pInv2 设为NULL
boost::scoped_array 和 boost::shared_array classes
void lock(Mutex* pm); //锁定pm所指向的互斥器
void unlock(Mutex* pm)//将互斥器解除锁定
-建立一个类来管理机锁 (资源在构造期间获得,字析构期间释放)
class Lock{
public:
explicit Lock(Mutex* pm)
:mutexPtr(pm)
{
lock(mutxPtr);
}
~Lock()
{
unlock(mutexPtr);
}
private:
Mutex *mutexPtr;
};
复制RAII对象必须一并复制它所管理的资源,所以资源的拷贝行为决定了RAII对象的拷贝行为
普遍儿常见的RAII class copying 行为是:抑制拷贝行为,施行引用计数法
std::string * stringArray = new std::string[100];
delete stirngArray;
stringArray所包含的一百个对象中99个没有被析构函数释放掉
当你使用new 时,会发生两件事 1.内存被分配 2.针对此内存会有一个或者多个构造函数被调用
delete必须知道内存中有多少个对象,才能去相应的调用多少次析构函数
在缺省的情况下,C++是以传值的方式传递对象到函数,除非我们自己指定,否则函数参数都是以实际参数的一份拷贝作为初值。这些拷贝的复件都是通过调用拷贝构造函数产生的,这样做使得传值调用的方式带来了很大的时间开销和内存开销(当然,这是基于一个比较大的对象来说的)
class Person
{
public:
Person();
virtual ~Peson;
private:
std::string name;
std::string address;
};
class Student:public Person{
public:
Student();
~Student();
private:
std::string schoolName;
std::string schoolAddress;
};
(1)Student plato;
(2)Student s(plato);
现在我们来分析一下这段代码,先提前说一下,这段代码的拷贝构造函数调用次数一定会让你感到很震惊。
最后得出结论,代码(2)总共调用了六次构造函数和六次析构函数
但是,如果传递的是引用的话,那就可以直接对对象进行操作,避免调用构造函数和析构函数。因为没有任何新的对象被建立,以引用传递也可以避免对象切割问题,当一个派生类以值传递的方式将会被声明为基类对象,基类的拷贝构造函数被调用,造成派生类的特化性质全被切割
为了解决切割问题,我们可以给函数的参数传入一个 const 的引用
class rational{
public:
Rational(int numrator =0,
int denominator = 1);
private:
int n,d;
friend const Rational operator* (const Rational &lhs,
const Rational &rhs);
};
}
我们需要搞清楚的一个问题是,引用既然是变量的别名,那就必须有一个变量存在,这个时候,如果我们的函数没有定义变量而直接就用引用,那么这个引用一定是存在问题的。这个时候,我们或许可以想到使用在函数中直接定义一个局部变量,然后有一个引用作为他的别名。但是我们需要考虑的问题是,当函数的生命周期结束,这个开辟在栈上的局部变量一定是要被销毁的。这个时候,我们的引用仍然指向这块变量,殊不知,这块变量早已经消失了,那么引用也就失去了它的价值。
-为了解决这个问题,我们只能采取另一种方案,即直接在堆上动态开辟内存空间给对象。这样做是可以避免函数栈桢自动销毁的问题,但是,还有另一个问题有待解决,这是什么呢?看一段代码
const Rational& operator* (const Rational& lhs,
const Rational& rhs)
{
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}
即使如此,我们还是需要付出代价,因未分配所得的内存将会以一个适当的构造函数进行初始化,既然 new 了一个对象,那么谁来对它进行 delete呢?
//这种情况下,如何 delete?
Rational w,x,y,z;
w = x*y*z; //与operator*(operator*(x,y),z) 相同
在上面的代码中,同一个语句调用了两次 operator* 因而使用了两次 new ,那么相对的也就需要两次 delete.但却没有合理的办法让 operator* 使用者进行那些delete调用,因为没有合理的办法让他们取得 operator* 返回的引用背后隐藏的那个指针,这样做绝对会导致资源泄漏。
面向对象守则要求,数据以及数据操作的那些函数应该被捆绑在一起,这意味着它建议member函数是较好的选择,但是很不幸的是这个建议不正确。
C++ 引用简介
int main()
{
void* vp;
char c;
int i;
float f;
double d;
vp = &c;
vp = &i;
vp = &f;
vp = &d;
}
int main()
{
int i=99;
void* vp =&i;
必须先转换类型
*((int*)vp) = 3;
}
//但是这样做也会存在一个问题,既然可以转化为 int 类型,那么同样的也就可以转化为 char ,double,这将改变已经分配给int 的存储空间大小,可能会引起程序崩溃
全局变量是在所有的函数体的外部定义的,程序的所有部分(甚至其他文件中的代码)都可以使用
- 使用extern 可以进行外部链接,使得另一个代码可以使用本代码中的变量。
局部变量经常被称为自动变量。因为他们在进入作用域时自动生成,离开作用域时自动消失。局部变量默认为auto,没必要显式声明。
使用 register 变量,我们不能得到或者计算 register 的地址,register 变量只能在一个块中声明,寄存器变量可以做函数形参
静态变量
通常情况下,我们如果在函数中定义了一个局部变量,会在函数作用域结束时自动消失,当我们下一次调用这个函数时,会重新创建该变量的存储空间,它的值也会被重新初始化。如果想要时局部变量的值在整个程序都会存在,我们可以定义函数的局部变量为static,并给他一个初值
#include
using namespace std;
void func() {
static int i = 0;
cout<<"i = "<<++i<int main()
{
for(int x=0;x<10;x++)
{
func();
}
}
}
//如果声明的不是static,那么每次都会打印出来1
static变量在作用域外不可用
简单的类型转换确实很奏效,但是有时候却会占用更大的内存空间,这可能会破坏其他的数据,因为它强迫编译器把一个数据看做是一个比它实际上更大的类型。这种强制类型转换通常用在指针的类型转换上,因为指针的大小在系统下都是固定的,但是有时候也会存在问题。
转换类型包括典型的非强制转换,窄化(有信息丢失)变换,使用void*的强制变换,隐式类型转换和类层次的静态定位
void func(int)
{}
int main(){ //使用static_cast 提升数据类型或者降低数据类型都是可以的
int i = 0x7fff; //但是一定要注意数据丢失
long l;
float f;
l=i;
f=i;
l = static_cast<long>(i);
f = static_cast<float>(i);
}
如果从const 转化为非 const 或从 volatile 转换为非 volatile ,可以使用 const_cast ,这是const_cast 唯一允许的转换
int main()
{
const int i = 0;
int *j = (int*)& i;
j = const_cast<int*>(& i);
volatile int k = 0;
int* u = const_cast<int*>(& k);
记住,如果取得了const 的地址,就可以生成一个指向 const 的指针,不用转换是不能将它赋给非 const指针的。
}
typedef 原类型名 别名
typedef unsigned long ulong
int* x,y;
typedef int* IntPtr
IntPtr x,y; //生成两个指针
typedef struct Structure3{
char c;
int i;
float f;
}Struture3;
int main()
{
Structure3 s1,s2;
Structure3 *p = &s1;
p-> c ='a';
}
enum color{
a++; //本质上这样是不对的
}; //必须加上;
void func1(int a[],int size);
void func2(int *a ,int size);
atoi() atol() atof()
int main(int argc,char* argv[])
{
for(int i=1;i
{
cout<<atoi(argv[i])<
要定义一个无参无返回值的函数
void (*funcptr)(); //记得函数指针的分辨
如果这样声明,就不一样了
void *funcPtr(); //不加()就会看成是一个返回值为 void* 的函数
void*(*(*fp1)(int))[10]; // fp1 是一个指向函数的指针,该函数接受一个整形参数并返回一个指向 10 个void 指针数组的指针
float(*(*fp2)(int,int,float))(int); //fp2是一个指向函数的指针,该函数接收三个参数且返回一个指向函数的指针,该函数
typedef double (*(*(*fp3)())[10])();
int (*(*f4())[10])();
如果一个类的一个函数被声明为虚函数,那么其派生类的对应函数也自动成为虚函数,这样一级级传递下去。虽然默认是虚函数,但是我们最好还是显式地声明一下,方便我们理解。
class Shape{ //虚函数的重写
public:
virtual void Draw(void);
};
class Rectangle;public Shape{
public:
virtual void Draw(void);
}
不能实例化出对象的类称为抽象类(那些把所有的构造函数都声明为private的类也是不能实例化的类)
- 抽象类的唯一目的就是让其派生类继承并实现它的接口方法。
- 如果该基类的虚函数声明为纯虚函数,那么该类就被定义为抽象基类。纯函数虚函数是在声明时将其初始化为0的函数
class Shape{
public:
virtual void Draw(void) = 0;
};
分析:函数名就是函数的地址,将一个函数初始化为0意味着函数的地址将为0,这就是在告诉编译器,不要为该函数编地址,从而阻止该类的实例化行为。
- 抽象基类的主要用途是:(接口与实现分离):不仅要把数据成员隐藏起来,而且还要把实现完全隐藏起来,只留一些接口给外部调用。即使将来实现改变了,接口仍然可以保持不变。
- 一般的信息隐藏就是把类的所有数据成员声明为private 或者protected,并提供相应的get 和set来访问对象的数据。抽象基类则进一步,它把数据和函数实现都隐藏在实现类中,而在抽象基类中提供丰富的接口函数供调用,这些函数都是public的纯虚函数,这样的抽象基类叫做接口类。
class IRectangle
{
virtual ~IRectangle(){}
virtual float Getlength()const = 0;
virtual void Setlength(float newLength) = 0;
virtual float Getwidth()const = 0;
virtual void Setwidth(float newWidth) = 0;
static IRectangle*_stdcall CreateRectangle(); //入口函数
void Destroy(){ delete this;}
};
class RectangleImp:public TRectangle{
public:
RectangleImp():m_length(1),m_width(1),m_color(0x00FFEC4D){}
virtual ~RectangleImp(){}
virtual float Getlength()const { return m_length;}
virtual void Setlength(float newLength) { m_length = newLength; }
private:
float m_length;
float m_width;
RGB m_color;
};
IRectangle* _stdcall IRectangle::CreateRectangle()
{
return new(nothrow)RectangleImp;
}
void main()
{
IRectangle *pRect = IRectangle::CreateRectangle();
}
(*(p->vptr[slotNum]))(p,arg-list); //指针当做数组来用,最后改写为指针运算
当许多的派生类因为继承了共同的基类而建立 is -a 关系时,没一个派生类的对象都可以被当成基类的对象来使用,这些派生类对象能对同一个函数调用做出不同的反应,这就是运行时多态。
void Draw(Shape *pShape)
{
pShape->Draw();
}
main()
{
Circle aCircle;
Cube aCube;
Sphere aSphere;
::Draw(&aCircle);
::Draw(&aCube);
::Draw(&aSphere);
}
在基类对象数组中存放派生类对象
Shape a(Point(1,1));
Circle b(Point(2,2),5);
Rectangle c(Point(3,3),Point(4,4));
Shape myShapes[3];
myShapes[0] = a;
myShapes[1] = b;
myShapes[2] = c;
for(int i=0;i<3;i++)
myShapes[i].Draw();
C++对象模型
- 非静态数据成员被放在每一个对象体内作为对象专有的数据成员
- 静态数据成员被提取出来放在程序的静态数据区内为该类所有对象共享,因此仅存在一份。
- 静态和非静态成员函数最终都被提取出来放在程序的代码段中并为该类的所有对象共享,因此每一个成员函数也只存在一份代码实体。
相近类型支持隐式类型转换
不相关类型一定是强制类型转换
对相关类型或者相近类型隐式类型转换
double d = static_cast<int>(i);
int *p = &i;
int j = reinterptret
int j = reinterpret_cast<int>(p) //不相关类型的转换,部分强制类型的转换
const int*p1 = p; //提醒把 const 属性去掉了
int* p2 =const_cast<int*>(p1)//去const属性--部分强制类型转换
typedef void (*Func)()
int Dosomething(int i)
{
cout<<"Do something"<void Test()
{
Func f =reinterpret_cast(Dosomething);
}
volatile const int a = 1; //通过查看汇编代码可以确定 a 就是 1 ,无法被修改,放在代码段也就是常量区,这样是一种优化,默认不会被修改,也就不会再内存中去找变量值。
class A{
}
B* p1 = (B*)p;
B* p2 = dynamic_cast(p);
如果是父类指针指向子类,那么访问子类元素就存在越界,但是如果是子类指针的,就可以访问到。
class A{
public:
A(int a);
:_a(a)
{
cout<<"build"<;
}
}
int main()
{
A a(10);
a b = 20; //构造函数和拷贝构造函数同时使用时,可以生成一个中间临时变量直接优化代码(生成匿名对象)在一个表达式里面就会优化
}
-在C++中,初始化和清楚地概念是简化库的使用的关键之处,并可以减少那些在客户程序员忘记去完成这些操作时会引发的细微错误
如果一个类有构造函数,那么编译器在创建对象时就自动调用这个函数。
- 成员函数默认传的第一个参数是 this 指针,所以构造函数传入的第一个参数是 this 指针,也就是调用这一函数的对象的地址,对构造函数来说,this 指针指向一个没有被初始化的内存块,构造函数的作用就是正确的初始化该内存块。
- 构造函数也可以像普通函数一样传递参数,指定对象该如何创建或设定对象初始值
出去安全性的考虑,应该尽可能在靠近变量的使用点处定义变量,并在定义时就初始化,通过减少变量在块中的生命周期,就可以减少该变量在块的其他地方被误用的机会,另外,程序的可读性也会增强,因为读者不需要跳到块的开头去确定变量的类型
struct X{
int i;
float f;
char c;
}
X x1 ={1,2.2,'c'};
当必须指定构造函数调用时,最好这样做
Y y1[] = {Y(1),Y(2),Y(3) };
默认构造函数就是不带任何参数的构造函数。当编译器需要创建一个对象又不知道任何细节时,默认的构造函数就显得非常重要
- 当有构造函数而没有默认构造函数时,定义的变量就会出现一个编译错误
- 因为由编译器生成的构造函数应该可以做一些智能化的初始化工作,比如把对象的所有内存置零。但是实际上编译器并不会这样做。因为这样做会增加额外的负担,而且使程序员无法控制。
- 解决办法,如果我们还是想要把内存初始化为0,那就得显式地编写默认的默认构造函数。
- 构造函数的重载,当我们想要初始化对象中不同个数的数据时,我们就可以同时在类中声明在类外定义多个构造函数。但是在进行构造函数重载时一定要注意一点:当有全部都有初始值得构造函数时就不要再定义其他的构造函数了,因为这样做会导致构造函数调用不清晰。
int f();
void f();
当编译器能够从上下文中唯一确定函数的意思时,如 int x = f();,这样当然是可以的,然而,在C语言中总是可以调用一个函数但忽略它的返回值,即调用了函数的副作用
一个联合也可以带有构造函数,析构函数,成员函数甚至访问控制
union U {
private:
int i;
float f;
public:
U(int a);
U(float b);
~U();
int read_int();
float read_float();
};
U::U(int a){ i=a;}
U::U(float b) { f =b; }
U::~U() { cout<<"U::~U()\n" }
int U::read_int() { return i; }
float U::read_float() { return f; }
int main()
{
U X(12), Y(1.9F);
cout<cout<
const 关键字现在用于各种场景,指针,函数变量,返回类型,类对象以及成员函数。
BUFSIZE 是一个名字,它只是在预处理期间存在,因此它不占用存储空间且能放在一个头文件里,目的是为使用它的所有编译但愿提供一个值。
const int bufsize = 100;
这样就可以在编译时编译器需要知道这个值的任何地方使用bufsize,同时编译器还可以执行常量折叠
通过包含头文件,可把const定义单独放在一个地方并把它分配给一个编译单元,C++中的 const 默认为内部连接,const 仅在const被定义过的文件里才是可见的,而在连接时不能被其他编译单元看到。
extern const int bufsize ;
编译器并不会为const 创建存储空间,相反它把这个定义保存在符号表中。但是,extern 强制进行了存储空间分配,由于 extern 意味着外部连接,因此必须分配存储空间
当众多的const 在多个cpp 文件中分配内存,容易引起连接错误,然而,const 默认内部连接,所以连接程序不会跨过编译单元连接那些定义,因此不会有冲突。在大部分场合使用内建数据类型的情况,包括常量表达式,编译都能执行常量折叠
如果不想让一个值改变,就应该声明成const,这不仅可以防止意外的更改提供安全措施,也消除了读存储器和读内存操作,使编译器产生的代码更有效。
const int* u;
int const* u;
这俩其实是一样的,都是一个指向常量的指针,因此指向的常量不能被修改
int const* u;
指针的指向在它的生命周期里不能被修改
const int const* u;
这种情况,不论是指向的变量还是指针本身的指向都不可以被修改。