static CWallet *wallet_;
wallet_=const_cast<CWallet *>(wallet);
二,静态成员函数
静态成员函数没有什么太多好讲的。
1.静态成员函数的地址可用普通函数指针储存,而普通成员函数地址需要用 类成员函数指针来储存。举例如下:
class base{
static int func1();
int func2();
};
int (*pf1)()=&base::func1;//普通的函数指针
int (base::*pf2)()=&base::func2;//成员函数指针
2.静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。
3.静态成员函数不可以同时声明为 virtual、const、volatile函数。举例如下:
class base{
virtual static void func1();//错误
static void func2() const;//错误
static void func3() volatile;//错误
};
最后要说的一点是,静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。
===============================================
==================================================================================
===============================================
二、面向过程程序设计中的static
1、全局静态变量
在全局变量之前加上关键字static修饰,全局变量就被定义成一个全局静态变量
- 内存中的位置:静态存储器(静态存储区在整个程序运行期间都存在的)
- 初始化:未初始化的全局静态变量会被程序自动化为0
- 作用域:全局静态变量在声明它的文件之外是不可见,即其他文件不能使用被static修饰的变量。只能在从定义处到文件结尾中被使用。
2、局部静态变量
- 内存中的位置:静态存储器
- 初始化:未经初始化的局部变量会被程序自动初始化为0
- 作用域:作用域仍为局部作用域,当定义它的函数或语句块结束的时候,作用域随之结束。
1. 在类的构造函数和析构函数中没有匹配的调用new和delete函数
两种情况下会出现这种内存泄露:一是在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内存;二是在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存
2. 没有正确地清除嵌套的对象指针
3. 在释放对象数组时在delete中没有使用方括号
方括号是告诉编译器这个指针指向的是一个对象数组,同时也告诉编译器正确的对象地址值病调用对象的析构函数,如果没有方括号,那么这个指针就被默认为只指向一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露。如果在方括号中间放了一个比对象数组大小还大的数字,那么编译器就会调用无效对象(内存溢出)的析构函数,会造成堆的奔溃。如果方括号中间的数字值比对象数组的大小小的话,编译器就不能调用足够多个析构函数,结果会造成内存泄露。
释放单个对象、单个基本数据类型的变量或者是基本数据类型的数组不需要大小参数,释放定义了析构函数的对象数组才需要大小参数。
4. 指向对象的指针数组不等同于对象数组
对象数组是指:数组中存放的是对象,只需要delete []p,即可调用对象数组中的每个对象的析构函数释放空间
指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了
5. 缺少拷贝构造函数
两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。
按值传递会调用(拷贝)构造函数,引用传递不会调用。
在C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。
所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符
6. 缺少重载赋值运算符
这种问题跟上述问题类似,也是逐个成员拷贝的方式复制对象,如果这个类的大小是可变的,那么结果就是造成内存泄露,如下图:
7. 关于nonmodifying运算符重载的常见迷思
a. 返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针
b. 返回内部静态对象的引用。
(
单例与采用静态变量引用一个对象的区别
亲爱的,是引用引用引用。。。不是指针不是指针不是指针。。。。。。。。。。哈哈哈哈哈哈哈哈哈哈哈
)单例的特点:
1. 保证某类只存在唯一实例。
2. 该类本身完成自身的初始化。
3. 获取该唯一实例的方式非常明确,可以通过该类本身定义的静态方法getInstance()获取该类的唯一实例引用。
静态变量定义某类的实例引用特点:
1. 该类的实例引用的静态变量可定义在任何文档类当中。
2. 获取该类的实例引用的静态变量,可以通过定义该静态变量的类名通过点语法进行访问该引用。
3. 任何位置可以对该静态变量进行重新赋值。
通过这两者方式的特点,我们可以很明显的看出两者之间的区别。(这一切都是基于某类只需要存在一个实例对象的前提来讨论)
首先静态变量方式不能确保某类的实例的唯一性,这样在项目中,可能因为在某个文档类中对该静态变量进行再次赋值,存不可意料的风险(这种风险可以规避)。同样的,因为静态变量的定义的位置不确定,所以需要协议商定,这些静态变量分类别进行定义在一个固定的位置(比如说某个专门存放静态变量方式的某类的对象的引用的文档类当中)。
而单例模式也就是静态变量方式创建一个类的实例引用所带来的缺陷的改善。首先解决引用的唯一实例可能被重新赋值的问题,单例模式中的getInstance()静态方法实现时,采用懒汉式创建一个对象(当然这只是创建方式的一种),规避了这一风险,无则创建,有则跳过创建。其次,getInstance()静态方法定义在该类的内部,获取该类对象的引用位置非常明确,无需额外的沟通商定,团队成员拿起即用。最后一个区别并不是很明显,声明一个静态变量,实际上,我们会直接对其进行初始化赋值,这样,在内存占用上,所占用的内存为该初始化赋值对象实际的内存。而单例模式可以通过懒汉创建法延迟该内存的占用,要知道,当一个静态变量只进行声明,而不进行初始化时,实际的内存占用只有4个字节(笔者个人推测,这四个字节只是一个指针地址所占用的内存空间)。
c. 返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收
解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,二应该是类型的返回值,即不是 int&而是int
8. 没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露
野指针:指向被释放的或者访问受限内存的指针。
造成野指针的原因:
- 指针变量没有被初始化(如果值不定,可以初始化为NULL)
- 指针被free或者delete后,没有置为NULL, free和delete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL.
- 指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针。
======================================================================================
=================================================================================================
======================================================================================
单例的特点:
1. 保证某类只存在唯一实例。
2. 该类本身完成自身的初始化。
3. 获取该唯一实例的方式非常明确,可以通过该类本身定义的静态方法getInstance()获取该类的唯一实例引用。
静态变量定义某类的实例引用特点:
1. 该类的实例引用的静态变量可定义在任何文档类当中。
2. 获取该类的实例引用的静态变量,可以通过定义该静态变量的类名通过点语法进行访问该引用。
3. 任何位置可以对该静态变量进行重新赋值。
通过这两者方式的特点,我们可以很明显的看出两者之间的区别。(这一切都是基于某类只需要存在一个实例对象的前提来讨论)
首先静态变量方式不能确保某类的实例的唯一性,这样在项目中,可能因为在某个文档类中对该静态变量进行再次赋值,存不可意料的风险(这种风险可以规避)。同样的,因为静态变量的定义的位置不确定,所以需要协议商定,这些静态变量分类别进行定义在一个固定的位置(比如说某个专门存放静态变量方式的某类的对象的引用的文档类当中)。
而单例模式也就是静态变量方式创建一个类的实例引用所带来的缺陷的改善。首先解决引用的唯一实例可能被重新赋值的问题,单例模式中的getInstance()静态方法实现时,采用懒汉式创建一个对象(当然这只是创建方式的一种),规避了这一风险,无则创建,有则跳过创建。其次,getInstance()静态方法定义在该类的内部,获取该类对象的引用位置非常明确,无需额外的沟通商定,团队成员拿起即用。最后一个区别并不是很明显,声明一个静态变量,实际上,我们会直接对其进行初始化赋值,这样,在内存占用上,所占用的内存为该初始化赋值对象实际的内存。而单例模式可以通过懒汉创建法延迟该内存的占用,要知道,当一个静态变量只进行声明,而不进行初始化时,实际的内存占用只有4个字节(笔者个人推测,这四个字节只是一个指针地址所占用的内存空间)。
=================================================================================
=======================================================================
C++的单例模式与线程安全单例模式(懒汉/饿汉)
1 教科书里的单例模式
我们都很清楚一个简单的单例模式该怎样去实现:构造函数声明为private或protect防止被外部函数实例化,内部保存一个private static的类指针保存唯一的实例,实例的动作由一个public的类方法代劳,该方法也返回单例类唯一的实例。
上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class
singleton
{
protected
:
singleton(){}
private
:
static
singleton* p;
public
:
static
singleton* instance();
};
singleton* singleton::p = NULL;
singleton* singleton::instance()
{
if
(p == NULL)
p =
new
singleton();
return
p;
}
|
这是一个很棒的实现,简单易懂。但这是一个完美的实现吗?不!该方法是线程不安全的,考虑两个线程同时首次调用instance方法且同时检测到p是NULL值,则两个线程会同时构造一个实例给p,这是严重的错误!同时,这也不是单例的唯一实现!
2 懒汉与饿汉
单例大约有两种实现方法:懒汉与饿汉。
-
- 懒汉:故名思义,不到万不得已就不会去实例化类,也就是说在第一次用到类实例的时候才会去实例化,所以上边的经典方法被归为懒汉实现;
- 饿汉:饿了肯定要饥不择食。所以在单例类定义的时候就进行实例化。
特点与选择:
-
- 由于要进行线程同步,所以在访问量比较大,或者可能访问的线程比较多时,采用饿汉实现,可以实现更好的性能。这是以空间换时间。
- 在访问量较小时,采用懒汉实现。这是以时间换空间。
3 线程安全的懒汉实现
线程不安全,怎么办呢?最直观的方法:加锁。
-
方法1:加锁的经典懒汉实现:
class singleton { protected: singleton() { pthread_mutex_init(&mutex); } private: static singleton* p; public: static pthread_mutex_t mutex; static singleton* initance(); }; pthread_mutex_t singleton::mutex; singleton* singleton::p = NULL; singleton* singleton::initance() { if (p == NULL) { pthread_mutex_lock(&mutex); if (p == NULL) p = new singleton(); pthread_mutex_unlock(&mutex); } return p; }
-
方法2:内部静态变量的懒汉实现
此方法也很容易实现,在instance函数里定义一个静态的实例,也可以保证拥有唯一实例,在返回时只需要返回其指针就可以了。推荐这种实现方法,真得非常简单。
class singleton { protected: singleton() { pthread_mutex_init(&mutex); } public: static pthread_mutex_t mutex; static singleton* initance(); int a; }; pthread_mutex_t singleton::mutex; singleton* singleton::initance() { pthread_mutex_lock(&mutex); static singleton obj; pthread_mutex_unlock(&mutex); return &obj; }
4 饿汉实现
为什么我不讲“线程安全的饿汉实现”?因为饿汉实现本来就是线程安全的,不用加锁。为啥?自己想!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
singleton
{
protected
:
singleton()
{}
private
:
static
singleton* p;
public
:
static
singleton* initance();
};
singleton* singleton::p =
new
singleton;
singleton* singleton::initance()
{
return
p;
}
|
是不是特别简单呢?
以空间换时间,你说简单不简单?
面试的时候,线程安全的单例模式怎么写?肯定怎么简单怎么写呀!饿汉模式反而最懒 [正经脸 ]!
=============================================================================
=======================================================================================
=============================================================================
内部类的实例化
如果内部类未声明为static,在实例化时首先需要new一个外部类的对象。并通过p.new Inner()的方式new 内部类,表明这个内部类指向该外部类。内部类的class类型为:Parent.Inner,而不是p.Inner,这个需要和new的方式区分开。
public class Test {
public static void main(String[] args) {
Parent p = new Parent();
Parent.Inner i = p.new Inner();
i.print();
}
}
class Parent {
class Inner {
public void print() {
System.out.println("xxx");
}
}
}
静态内部类的实例化
静态内部类与普通内部类的区别在于,静态内部类的对象是不指向与某个具体的外部类对象,所以在创建对象时不需要创建外部类对象。并且在new的时候是通过 new Parent.Inner()方式,而不是Parent.new Inner()。不要和内部类的实例化搞混了。class的声明和内部类是一样的,都是Parent.Inner
public class Test {
public static void main(String[] args) {
Parent.Inner i = new Parent.Inner();
i.print();
}
}
class Parent {
staticclass Inner {
public void print() {
System.out.println("xxx");
}
}
}
==========================================================================
=============================================================
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块 内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的 大小,系统会自动的将多余的那部分重新放入空闲链表中。
============================================================
=============================================================================
=============================================================
说明:笔者的操作系统是32位的。
class A{};
sizeof(A)=1;
class B
{
public:
};
sizeof(B)= 1;
//kingkingkingKINGKINGKINGKINGKINGKINGKIGNKIGNGKINGKIGNKIGNG
//kingkiingkingKINGKINGKIGNGKINGKINGKINGKINGKINGKINGKINGKINGKING
classC
{
};
sizeof(C)= 4;
classD
{
};
sizeof(D)= 4;
classE
{
};
sizeof(E)= 8
class F: public E
{
};
int F::s_data=100;
sizeof(F)= 8;
class G: public E
{
};
class H : public G
{
};
sizeof(G)= 16;
sizeof(H)= 20;
class I: public D
{
};
sizeof(I)= 4;
(一)类内部的成员变量:
普通的变量:是要占用内存的,但是要注意内存对齐(这点和struct类型很相似)。
static修饰的静态变量:不占用内存,原因是编译器将其放在全局变量区。
从父类继承的变量:计算进子类中。
(二)类内部的成员函数:
非虚函数(构造函数、静态函数、成员函数等):不占用类对象的内存,放在代码区只有一份,供所有类对象共用。
虚函数:要占用4个字节(32位的操作系统),用来指定虚拟函数表的入口地址。跟虚函数的个数没有关系。父类子类工享一个虚函数指针。
测试代码如下:
- "font-size:18px;">
-
- #include
-
- using
namespace std; -
- class
A {}; -
- class
B - {
- public:
-
B() {} -
~B() {} -
void MemberFuncTest( int para ) { } -
static void StaticMemFuncTest( int para ){ } - };
-
- class
C - {
-
C(){} -
virtual ~C() {} - };
-
- class
D - {
-
D(){} -
virtual ~D() {} -
virtual int VirtualMemFuncTest1()=0; -
virtual int VirtualMemFuncTest2()=0; -
virtual int VirtualMemFuncTest3()=0; - };
-
- class
E - {
-
int m_Int; -
char m_Char; - };
-
- class
F : public E - {
-
static int s_data ; - };
- int
F::s_data=100; -
- class
G : public E - {
-
virtual int VirtualMemFuncTest1(int para)=0; -
int m_Int; - };
- class
H : public G - {
-
int m_Int; - };
-
- class
I : public D - {
-
virtual int VirtualMemFuncTest1()=0; -
virtual int VirtualMemFuncTest2()=0; - };
-
- int
main( int argc, char **argv ) - {
-
cout<<"sizeof( A ) = "<<sizeof( A )<<endl; -
cout<<"sizeof( B ) = "<<sizeof( B )<<endl; -
cout<<"sizeof( C ) = "<<sizeof( C )<<endl; -
cout<<"sizeof( D ) = "<<sizeof( D )<<endl; -
cout<<"sizeof( E ) = "<<sizeof( E )<<endl; -
cout<<"sizeof( F ) = "<<sizeof( F )<<endl; -
cout<<"sizeof( G ) = "<<sizeof( G )<<endl; -
cout<<"sizeof( H ) = "<<sizeof( H )<<endl; -
cout<<"sizeof( I ) = "<<sizeof( I )<<endl; -
- #if
defined( _WIN32 ) -
system("pause"); - #endif
-
return 0; - }
-
-
Windows7 32位 VC 2010运行结果:
Linux(cent os 6.2 32位)运行结果:
=============================================
===================================================================
=============================================
浅谈类对象和类指针
代码:
分析:
定义类对象基本格式是:Student a;在定义之后就已经为a这个对象分配了内存,且为内存栈;
定义类指针基本格式是:Student *b = new Student();在定义*b的时候并没有分配内存,只有执行new后才会分配内存,且为内存堆。
问题:
(1)类对象和类指针区别
A: 定义
——类对象:利用类的构造函数(构造函数:对类进行初始化工作)在内存中分配一块区域(包括一些成员变量赋值);
——类指针:是一个内存地址值,指向内存中存放的类对象(包括一些成员变量赋值;类指针可以指向多个不同的对象,这就是多态);
B: 使用
——引用成员:对象使用“.”操作符,指针用“->”操作符;
——生命周期:若是成员变量,则由类的析构函数来释放空间;若是函数中临时变量,则作用域是函数体内;而指针则需要利用delete在相应的地方释放分配的内存块。
注意:new与delete成对存在!!!
C: 存储位置
——类对象:用的是内存栈,是个局部的临时变量;
——类指针:用的是内存堆,是个永久变量,除非你释放它。
D: 多态
——指针可以实现多态,直接用对象不行。
F: 访问方式
——指针变量是间接访问,但可实现多态(通过父类指针可调用子类对象),并且没有调用构造函数;
——直接声明可直接访问,但不能实现多态,声明即调用了构造函数(已分配了内存)。
(2)类对象和类指针联系
——在类的声明尚未完成的情况下,可以声明指向该类的指针,但是不可声明该类的对象;
——父类的指针可以指向子类的对象。
(3)指针与多态
——有前面可知:类指针是一个指向内存中存放类对象的内存地址值,那么这个指针可以指向多个不同的对象,这就是多态;
拓展:指针与虚函数
——要发挥虚函数的强大作用,必须使用指针来访问对象。
——当类是有虚函数的基类,Func是它的一个虚函数,则调用Func时:
类对象:调用的是它自己的Func;
类指针:调用的是分配给它空间时那种类的Func。
(4)什么情况使用类对象与类指针?
——其实作用基本一样 都是为了调用类的成员变量和成员函数用的;
——当你希望明确使用这个类的时候,最好使用对象;
如果你希望使用C++中的动态绑定,则最好使用指针或者引用,指针和引用用起来更灵活,容易实现多态等。
(5)指针好处
——第一,实现多态。
——第二,在函数调用,传指针参数。不管你的对象或结构参数多么庞大,你用指针,传过去的就是4个字节。如果用对象,参数 传递占用的资源就太大了。
======================================================================================
=====================================================================================================
======================================================================================
静态变量并不是说其就不能改变值,不能改变值的量叫常量。 其拥有的值是可变的 ,而且它会保持最新的值。说其静态,是因为它不会随着函数的调用和退出而发生变化。即上次调用函数的时候,如果我们给静态变量赋予某个值的话,下次函数调用时,这个值保持不变。 静态变量 类型说明符是static。 静态变量属于静态存储方式,其存储空间为内存中的静态数据区(在 静态存储区内分配存储单元),该区域中的数据在整个程序的运行期间一直占用这些存储空间(在程序整个运行期间都不释放),也可以认为是其内存地址不变,直 到整个程序运行结束(相反,而auto自动变量,即动态局部变量,属于动态存储类别,占动态存储空间,函数调用结束后即释放)。静态变量虽在程序的整个执 行过程中始终存在,但是在它作用域之外不能使用。 另外,属于静态存储方式的量不一定就是静态变量。 例如:外部变量虽属于静态存储方式,但不一定是静态变量,必须由 static加以定义后才能成为静态外部变量,或称静态全局变量。 所有的全局变量都是静态变量,而局部变量只有定义时加上类型修饰符static,才为局部静态变量。 静态变量可以在任何可以申请的地方申请,一旦申请成功后,它将不再接受其他的同样申请
=================================================================================
============================================================================================================
=================================================================================
产生野指针三个原因:
(1)指针变量创建时候没有被初始化:任何指针变量在创建的时候,不会自动成为NULL指针,它的默认值是随机的,因此该指针就会成为一个野指针,可能指向一块不可使用的内存空间。
例如char *p; 这样创建一个指针p,指向一个随机的内存地址空间
所以指针在创建的时候要被初始化,可以讲其初始化为NULL,或指向合法的内存空间
比如 char *p = NULL ; 或 char *p = new char; //这个时候p就不会是一个野指针
(2)delete或free指针之后没有把指针设置为NULL:delete和free只是把指针所指的内存空间释放掉,而没有对指针本身进行释放。
比如char *p = new char(4) ; delete[] p; //这时候指针p所指的内存空间被释放,但是指针p本身不为空,但是指针p所指向的内存空间已经不能使用,造成了野指针。正确的做法是及时的把指针p赋值为NULL
例如下面这个程序
char *p = (char *)malloc(100);
strcpy(*p, "hello");
free(p);
if(p != NULL){
printf("not NULL\n");
}
结果输出”not NULL“,验证上面的结论,应该在free之后马上把p = NULL。
(3)指针操作超过了指向内存空间的作用范围:当指针越界之后也会变成一个野指针
=========================================================
=======================================================================
=========================================================
野指针指针操作超越变量作用域
class
A {
public
:
void
Func(
void
){ cout << “Func of
class
A” << endl; }
};
class
B {
public
:
A *p;
void
Test(
void
) {
A a;
p = &a;
// 注意 a 的生命期 ,只在这个函数Test中,而不是整个class B
}
void
Test1() {
p->Func();
// p 是“野指针”
}
};
|
由于a的生命周期是在void Test(
void
)函数结束时就应该被释放,所以你再引用指针p的时候它指向的内存已经被释放了,所以p已经是野指针了。
下面再来说一下内存泄漏
内存泄漏是指我们在堆中申请了一块内存,但是没有去手动的释放内存,导致指针已经消失,而指针指向的东西还在,已经不能控制这块内存,
所以就是内存泄漏了,看下面的例子。
void remodel(std::string &str)
{
std::string *ps = new std::string(str);
//内存泄漏了。
return;
}
建立了一个指针ps,这个指针是局部变量,放置在栈中,函数结束其生命周期结束,但是申请的内存没有被释放,造成内存泄漏
=============================================================================================================
===============================================================================================
本篇文章,将提到4个概念:
1、普通变量
2、指针变量
3、内存(内存空间)
4、地址
我们先看内存是什么?内存是实实在在的硬件,可以存放数据!在我们的一块可编程的芯片的内部有大把的内存。
形象一点,内存就像一个个的小格子,每个格子的大小是一个字节,可以存放一个字节的数据。
那这么多内存如何区分呢?那就得靠地址。地址是内存的标识,每一个地址都对应一个内存。所以内存和地址是一一对应密不可分的。
接着看,什么是普通变量?
如 char a; 就是一个普通变量。普通变量a其实是语言本身创造了,是为了更方便的表示内存。我们对a进行访问其实就是直接对内存进行访问。至于a表示的内存的地址是多少,程序员一般不用关心。编译器会自动分配地址,也就是常说的为a分配一个地址。如果想知道a的地址也可以通过&a得知。
再看指针变量,他和普通变量的区别在于,普通变量是和一块内存空间关联。而指针变量却是和两块内存空间想关联:
1、保存指针变量本身的空间,这个空间大小是固定的,32位系统中是4个字节。
2、指针指向的内存空间。
如char* a; 指针变量a,他本身需要一个空间,也就是上面说的(1)。
而(1)这个空间存放的内容是另一个内存空间的首地址。指针变量可以通过改变自己去访问其他地方的内存空间。
如果说普通变量有两种形态:
1、a 表示一块内存空间
2、&a 表示当前内存空间的地址
那么指针变量就具有3种形态:
如果硬要说第4种形态,就是是p->x,这种形态出现在结构体变量访问自己的成员的时候。p->x结合之后看出一个整体,其实就是代表x对应的那块内存。这里需要注意的是“->”这个符号,不要理解成指针p指向x,而应该将p->x看成一个整体,“->”只是一个操作符将p和x结合到一起,就可以表示x所对应的内存。
以上结论是具有通用性的。思考下,如果p是结构体指针,那么*p又是什么呢?虽然说*p的值意义不大。*p就是对应整个结构体的变量内存空间。这个普通的*a解释起来是一样的——表示指针a所指向的变量对应的整个内存空间。(可以用程序证明这一点,为了文章主线,证明过程我想放到《解引用结构体指针的值是什么》这篇文章中单独讲述)
以上就简单介绍完了普通变量、指针变量、内存(内存空间)、地址,这四个概念,并且详细对比了普通变量和指针变量的区别。
接下来,看看地址与指针以及数字常量的区别。
1、指针也称为指针变量,地址是个常量。指针指向地址。地址仅仅是内存的标号。
2、如何把一个数强制转换为一个指针类型。
把一个数强制转换成指针类型:如int*(0)那么,那么int*(0)是一个指针,而不是地址。(编译器会为int*(0)分配内存)
其实就是: int* p = int*(0); 那么p 就相当与这个int*(0)。
这个指针指向0这个地址。所以此时0表示地址,int*(0)是指针。
3、虽然指针不是地址,但是和地址相匹配,可以将地址赋值给指针。当然也可以直接给指针赋值一个数字常量。但是一般不要这么做,应为这个数所代表的地址,可能是你不该访问的,可能会导致段错误。
所以指针的赋值一般是将变量取地址赋值给指针,或者通过指针赋值给指针(p = &a 或者 p = p1)。
最后,简单描述下。指针与内存空间的关系——指针是内存空间的控制器。不同类型的指针,拥有不同的内存管理能力。如int*a 和
char* a管理内存的方式是不一样的。进一步理解可以升入到结构体变量指针(类变量指针),各自有着各种管理内存的方法。
如果你能理解指针是内存的控制器,那么就能理解链表的实现。链表会在另外一篇博客详细介绍。
指针和内存进一步的分析,我也放到另一篇博客~~就到这里~~
指针指向了一块内存空间 !!!!!!!!!!!!!!!!!我操我操我操
============================================================================================================
类指针:是一个内存地址值,指向内存中存放的类对象(包括一些成员变量赋值;类指针可以指向多个不同的对象,这就是多态);
1 指针简介
指针(pointer)是“指向(point to)”另外一种类型的复合类型,它实现了对其它对象的间接访问。定义指针类型的方法将声明符写成*d的形式,其中d是变量名,如以下代码声明了一个整形指针:
int *ip1;
2 指针赋值
对指针进行赋值只能使用以下四种类型的值:(1)空指针(2)类型匹配的对象的地址(3)同类型的另一个有效指针(4)另一对象之后的下一地址。
2.1 空指针
空指针(null pointer)不指向任何对象。
(1)赋值为空指针
有以下几种方法可以将指针赋值为空指针。
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;
其中,nullptr是C++11新标准刚刚引入的一种方法。
(2)不能赋值为整形变量,但是可以赋值为整形常量
把整形变量直接赋给指针是错误的操作,如
int zero = 0;
int *p1 = zero;
如果将zero声明为常量值,则可以用其对指针进行赋值:
const int zero = 0;
int *p1 = zero;
(3)未定义的指针
如果只是声明了一个int指针,而未对其进行定义,如
int *p4;
则此时该指针的值是0xCCCCCCCC,如图2-1所示。
图2-1 空指针与未定义指针
2.2 类型匹配的对象的地址
可以将指针赋值为其类型匹配的对象的地址。
int one = 1;
int *p5 = &one;
从图2-2中可以看到,此时p7的值是0x005CFB98,而0x005CFB98的内存中保存的值是4个字节的int类型的数据,其值为1。
图2-2 类型匹配的对象的地址
2.3 同类型的另一个有效指针
可以将指针赋值为同一类型的另一个有效指针。
int *p6 = p5;
此时,可以从图2-3中看到,p6的值与p5的值相同,都是0x005CFB98。
图2-3 同类型的另一个有效指针
2.4 另一对象之后的下一地址
还可以将指针赋值为同类型对象的下一个地址。
int *p7 = p5+1;
从图2-4中可以看到,p7的值是0x005CFB9C,也就是p5的值0x005CFB98加上4个字节。
图2-4 另一对象之后的下一地址
===============================================================================================
关于int类型数据在内存中的高低位存储问题
最近在给学生讲课的时候,学生问到,对于一个c语言编写的程序,一个int类型的数据,在内存中是如何存储的。
例如:int类型的1在内存中占用4个字节,那这4个字节具体怎么存储呢?
目前市面上大部分书籍说的都是数字的字节表示形式,按照二进制的方式进行存储。学生就理所当然的认为是按照下面方式进行存储的。
第1字节 第2字节 第3字节 第4字节
00000000 00000000 00000000 00000001
但是我告诉他们实际上并不是这样存储的,而是低位在前,高位在后的方式存储的,也就是按照下面的方式
第1字节 第2字节 第3字节 第4字节
00000001 00000000 00000000 00000000
学生们就觉得有点不太好理解,于是就写了一段小程序来检验一下,看看是否是我说的这种方式进行存储。
- #include <stdio.h>
- int main(){
- int i = 1;
- unsigned char * p = (unsigned char *)(&i);
- printf("第1字节:%d,第2字节:%d,第3字节:%d,第4字节:%d\n",*p,*(p+1),*(p+2),*(p+3));
- }
于是他们按照我的这个思路,对所有的基本数据类型都做了一个检测,并且对结构体等复杂类型的内存存储方式都做了一个检验,对数据类型在内存中的存储方式的理解又深入了一步。
=============================================================================
=================================================================================================
=============================================================================
int 变量依赖于编译器字长。 编译器字长有16位,32位,64位三种。 1 对于16位编译器,int的长度为2字节,即2进制的16位。 写作16进制时,4位二进制用一位16进制数表示,所以表示int需要用4位16进制数。如0xABCD。 2 对于32位和64位编译器,int长度为4字节,即2进制32位。 写作16进制时,需要用8位16进制数表示,如0x12345678。=================================================================================
==========================================================================================================
=================================================================================
程序中通常包含着静态内存和栈内存。静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量(全局变量)。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在定义的程序块运行时才存在,程序退出,栈对象也随即销毁;static对象和全局对象则是在程序结束时销毁。除了静态内存和栈内存,程序还拥有一块内存池,这部分也就是称为堆。在使用堆空间是就需要使用动态内存分配。
内存泄漏:是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
对于服务器程序及需要长时间运行的程序来说,检测和解决内存泄漏是程序员必备的技能。
1. 内存泄漏出现的情况总结
首先总结一下c++在语法上的错误使用导致的内存泄漏,所以在编写程序时,就尽量避免错误的编写。
(1) 正确的使用new和delete函数,需要注意的是new和delete要匹配使用,对于初学者这种情况是最常出现的。一般出错的地方像如下的例子,在指针p的值被另一个函数所使用。
char * FunA()
{
char *p = new char;
return p;
}
void FunErrorB()
{
char *b = FunA();
//忘记delete p
}
(2) 释放对象数组时,没有使用delete[]。如例子所示:
Void FunErrorA()
{
Char *p = new char[10];
Delete p;
}
(3) 双指针释放错误,存在指针释放的遗漏。如例子正确的释放一个双指针
Void FunRightA()
{
Char **p = new char*[10];
For(int i=0;i<10;i++)
{
p[i] = new char[10];
}
If(p!=nullptr)
{
For(int i=0;i<10;i++)
{ Delete []p[i];
p[i] = nullptr;
}
Delete []p;
p = nullptr;
}
}
(4)缺少拷贝构造函数。在类里存在成员变量是指针时,在进行赋值=运算和按值传参时,必须重载拷贝构造函数,重新实现其指针拷贝的部分.
(5)没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。(哈哈哈哈哈哈哈哈呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵呵)
(6)调用库存在内存泄漏。在使用由个人包装或者未完全测试的库时,要确定此库对本程序不存在性能的影响。
当遇到内存泄漏时,我们该如何进行确认出现内存泄露和定位内存泄露的位置。
============================================================================
=============================================================================================
============================================================================
静态变量 static
================================================================================================
=========================================================================================================
===================================================================================================================
.拷贝构造函数
拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。
当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象
A a;
A b(a);
A b=a; 都是拷贝构造函数来创建对象b
强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!
<span style="font-size:14px;">class A;
A a;
A b=a; //调用拷贝构造函数(b不存在)
A c(a) ; //调用拷贝构造函数
/****/
class A;
A a;
A b;
b = a ; //调用赋值函数(b存在)</span>
我操我操我操我操我操我操!!!!!!!!!!!!!!!!逗我玩呢
=====================================================================
对于拷贝构造函数和赋值构造函数的理解
昨天晚上在看智能指针的时候,我发现自己连一个拷贝构造函数和赋值构造函数都写不出来,自己就尝试写了一个版本,结果发现错误百出,对于拷贝构造函数和赋值构造函数的理解仅仅停留在理论的方面,而不知其中太多的内涵。
比如我们都知道拷贝构造函数和赋值构造函数最大的不同在于:
拷贝构造是确确实实构造一个新的对象,并给新对象的私有成员赋上参数对象的私有成员的值,新构造的对象和参数对象地址是不一样的,所以如果该类中有一个私有成员是指向堆中某一块内存,如果仅仅对该私有成员进行浅拷贝,那么会出现多个指针指向堆中同一块内存,这是会出现问题,如果那块内存被释放了,就会出现其他指针指向一块被释放的内存,出现未定义的值的问题,如果深拷贝,就不会出现问题,因为深拷贝,不会出现指向堆中同一块内存的问题,因为每一次拷贝,都会开辟新的内存供对象存放其值。
下面是浅拷贝构造函数的代码:
#include <iostream> using namespace std; class A { private: int* n; public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) { n = a.n; cout<<"copy constructor is called\n"; } ~A() { cout<<"destructor is called\n"; delete n; } void get() { cout<<"n[0]: "<<n[0]<<endl; } }; int main() { A* a = new A(); A b = *a; delete a; b.get(); return 0; }
运行结果如下:
下面是深拷贝构造函数的代码:
#include <iostream> #include <string.h> using namespace std; class A { private: int* n; public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) { n = new int[10]; memcpy(n, a.n, 10); //通过按字节拷贝,将堆中一块内存存储到另一块内存 cout<<"copy constructor is called\n"; } ~A() { cout<<"destructor is called\n"; delete n; } void get() { cout<<"n[0]: "<<n[0]<<endl; } }; int main() { A* a = new A(); A b = *a; delete a; b.get(); return 0; }
运行截图如下:
但是赋值构造函数是将一个参数对象中私有成员赋给一个已经在内存中占据内存的对象的私有成员,赋值构造函数被赋值的对象必须已经在内存中,否则调用的将是拷贝构造函数,当然赋值构造函数也有深拷贝和浅拷贝的问题。当然赋值构造函数必须能够处理自我赋值的问题,因为自我赋值会出现指针指向一个已经释放的内存。还有赋值构造函数必须注意它的函数原型,参数必须是引用类型,返回值也必须是引用类型,否则在传参和返回的时候都会再次调用一次拷贝构造函数。
#include <iostream> #include <string.h> using namespace std; class A { private: int* n; public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) //拷贝构造函数的参数一定是引用,不能不是引用,不然会出现无限递归 { n = new int[10]; memcpy(n, a.n, 10); //通过按字节拷贝,将堆中一块内存存储到另一块内存 cout<<"copy constructor is called\n"; } A& operator=(const A& a) //记住形参和返回值一定要是引用类型,否则传参和返回时会自动调用拷贝构造函数 { if(this == &a) //为什么需要进行自我赋值判断呢?因为下面要进行释放n的操作,如果是自我赋值,而没有进行判断的话,那么就会出现讲一个释放了的内存赋给一个指针 return *this; if(n != NULL) { delete n; n == NULL; //记住释放完内存将指针赋为NULL } n = new int[10]; memcpy(n, a.n, 10); cout<<"assign constructor is called\n"; return *this; } ~A() { cout<<"destructor is called\n"; delete n; n = NULL; //记住释放完内存将指针赋为NULL } void get() { cout<<"n[0]: "<<n[0]<<endl; } }; int main() { A* a = new A(); A* b = new A(); *b = *a; delete a; b->get(); return 0; }
运行截图如下:
如果我们在赋值构造函数的形参和返回值不用引用类型,代码如下:
#include <iostream> #include <string.h> using namespace std; class A { private: int* n; public: A() { n = new int[10]; n[0] = 1; cout<<"constructor is called\n"; } A(const A& a) //拷贝构造函数的参数一定是引用,不能不是引用,不然会出现无限递归 { n = new int[10]; memcpy(n, a.n, 10); //通过按字节拷贝,将堆中一块内存存储到另一块内存 cout<<"copy constructor is called\n"; } A operator=(const A a) //传参和返回值设置错误 { if(this == &a) return *this; if(n != NULL) { delete n; n == NULL; } n = new int[10]; memcpy(n, a.n, 10); cout<<"assign constructor is called\n"; return *this; } ~A() { cout<<"destructor is called\n"; delete n; } void get() { cout<<"n[0]: "<<n[0]<<endl; } }; int main() { A* a = new A(); A* b = new A(); *b = *a; delete a; b->get(); while(1) {} return 0; }
运行截图如下:
多了两次的拷贝构造函数的调用和两次析构函数的调用。
==============================================================+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
=============================================================
哈哈啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊
当a是一个指针的时候,*a就是这个指针指向的内存的值 在定义的时候加了*的都是指针变量,都是一个地址。 在赋值的时候加了*的都是表示这个指针指向内存的值,在等号前面就是给这个值赋值,后面就是取这个值============================================================
所以拷贝构造函数说的是对象本身
如上:
A* a=new A();
A* b =new A();
*b=*a;
*a和*b都是对象本身
如果b=a说的是指针本身 占用的内存是不一样的
指针b等于a,他们指向同一块内存
====================================================================================
===================================================================================