类的动态与静态类型

静态类型和动态类型

C++中的对象分为静态类型和动态类型,而简单来说,静态类型是在编译时确定的,而动态类型则是在运行时确定的,就如下面例子

static int a;//static
int * pt = new int;//dynamic

但这里我们读者是否又会疑惑呢?变量a编译时并没有分配相应的值,如果运行时赋值,那么是否也应属于动态类型呢?实则不然。我们用下面的一个小程序,了解一下a为何是静态

#include<iostream>

using namespace std;//bad habit don't follow

int main(){
    int a;
    cout << a;
    return 0;
}

以上代码我们用gcc编译,运行

结果是4248811,但是我们并没有赋值!在C++中,普通变量编译时会指向随机的一块内存,那么我们在前面加上static关键字

#include<iostream>

using namespace std;//bad habit don't follow

int main(){
    static int a;
    cout << a;
    return 0;
}

这里又不一样了!这次a变成了0!实际上,对于静态变量,如果初始化时没有进行赋值,那么默认是0,所以无论是否在定义时赋值,静态变量在编译时就已经确定

到这里我们已经初步了解静态和动态类型,而类中的动态静态又有着些许不同之处

类中的动静态

静态

//bad.h
#include<iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_

class bad{
private:
    char * str;
    int len;
    static int num_string;
public:
    bad(const char *);
    bad();
    ~bad();

friend:
	friend std::ostream & operator<<(std::ostream &os,const bad & st);
	}

};
#endif

这里我们在bad.h中放入了bad类定义,为了让读者更好理解类中的普通变量,动态变量,静态变量有何区别,我们定义了三个变量,一一对应

在类中静态变量比较特殊,普通的静态变量的作用域位于自身文件,即使加上extern关键字依然不可在其他文件访问,而类中的静态变量则自然作用于类,最为特殊的是,一个类不论是有多少个对象,都有且只有一个静态变量,所有对象的静态变量都只指向一个副本,换而言之,牵一发而动全身,这个特性让类的使用更加方便灵活,但反之,同样极易造成不易察觉的bug

同样在使用方法上,静态类成员也有很多需要加以注意的地方,这里进行总结,不再一一赘述

static 关键字最基本的用法是:

  • 1、被 static 修饰的变量属于类变量,可以通过类名.变量名直接引用,而不需要 new 出一个类来
  • 2、被 static 修饰的方法属于类方法,可以通过类名.方法名直接引用,而不需要 new 出一个类来

被 static 修饰的变量、被 static 修饰的方法统一属于类的静态资源,是类实例之间共享的,换言之,一处变、处处变

在 C++ 中,静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量实现多个对象之间的数据共享不会破坏隐藏的原则,保证了安全性还可以节省内存

静态成员的定义或声明要加个关键 static

静态成员可以通过双冒号来使用即 <类名>::<静态成员名>

静态成员不可以在类声明时初始化,因为声明只是描述了如何分配内存,没有实际上分配内存,除非是内置的const整数类型或者枚举量

 动态

类的实现

#include<cstring>
#include"bad.h"

int bad::num_string = 0;

bad::bad(const char * a){

    len = std::strlen(a);
    str = new char[len + 1];
    std::strcpy(str,a);
    num_string++;
    std::cout << num_string << ":\"" << str << "\"object created\n";

}

bad::~bad(){
        
    std::cout << "\"" << str << "\"object deleted\n";
    num_string--;
    std::cout << num_string << "left\n";
    delete [] str;
}
bad::bad(){

    len = 4;
    str = new char[4];
    std::strcpy(str,"C++");
    num_string++;
    std::cout << num_string << ": \"" << str << "\"deafult object created\n";

}

std::ostream & operator<<(ostream & os,const bad & st){
	
	os << st.str;
	return os;
}

需要注意,这个类是有很大的缺陷,稍后我们将通过一个例子揭示

程序解释

首先,bad()bad(const char *)互为函数重载,两者实现基本相同,但需要注意,strcpy函数的用法,实际上strcpy传承于C语言的string库,这里总结一下

C 库函数 char *strcpy(char *dest, const char *src) 把 src 所指向的字符串复制到 dest

需要注意,如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况

  • dest -- 指向用于存储复制内容的目标数组。
  • src -- 要复制的字符串。

析构函数~bad()则在类的生命周期结束后,使用delete释放相应类成员的所占内存

有趣的错误 

上文说到我们的类设计是有很大的问题的,那究竟有什么问题呢?

#include"bad.cpp"

void call_1(bad &);
void call_2(bad);

using std::cout;

int main(){
	using std::endl;
	{
		cout << "starting an inner block\n";
		bad a1("A");
		bad a2("B");
		bad a3("C");
		
		call_1(a1);
		cout << "a1:" << a1 << endl;
		call_2(a2);
		cout << "a2:" << a2 << endl;
		
		bad a4 = a3;
		cout << "a4:" << a4 << endl;
		
		bad a5;
		a5 = a1;
		cout << "a5:" << a5 << endl;
        
		cout << "EXIT";
	}
	cout << "end";
	
	return 0;
}

void call_1(bad & rsb){
	
	cout << "reference\n";
	cout << "\"" << rsb << "\"\n";

}

void call_2(bad r){
	
	cout << "value\n";
	cout << "\"" << r << "\"\n";
}

实例有助于我们发现这个问题,上述代码正是如此

我们用gcc编译运行,结果如下

注意!有些较新的编译器可能无法通过编译,主要原因在于GPF(表明程序正试图访问被禁止的内存单元),同时其中一些非标准字符的出现,也因环境的不同而不同,读者无需担心这种差异

我们很容易发现,用于计数的num_string变得非常异常,为什么这么说呢?

我们看着这一段类的实现

bad::bad(const char * a){

    len = std::strlen(a);
    str = new char[len + 1];
    std::strcpy(str,a);
    num_string++;
    std::cout << num_string << ":\"" << str << "\"object created\n";
}

这是类的构造函数,默认构造函数与之相似,不再过多赘述,这里,我们每次声明一个对象,都会更新一次num_string,使之加1

同样,析构函数也同样,每当销毁一个对象,就会自动减1

所以,按道理来讲,num_string不会出现负值,但运行结果却超出了我们的意料,这是为什么?

我们尝试来定位一下错误,显而易见,我们的程序有两个对象,没有经过构造函数,绕过构造函数直接被定义,问题出在哪?

这就不得不提到一个特殊类成员函数——复制构造函数

回到代码的实现

bad a4 = a3;
cout << "a4:" << a4 << endl;
		
bad a5;
a5 = a1;
cout << "a5:" << a5 << endl;

这两段实现看似十分相似实际上大有不同,我们先来解释第一段,这里直接用a3定义了a4,看似很平常的定义,但对于编译器,这一段实际等价于

bad a4 = bad(const bad & a3);

但我们实际上没有定义过这种函数,编译器为我们隐式声明,而与之类似的函数,我们统称为复制构造函数,当我们尝试用一个对象初始化另一个对象时,编译器会调用复制构造函数,如果我们没有提供复制构造函数,编译器会提供默认复制构造函数,创造一个对象,并把另一对象的非静态成员逐一赋值,这也是为什么会有两个对象莫名其妙地绕过构造函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值