您的位置:首页 > 编程语言 > C语言/C++

c++入门学习(类)

2005-03-14 21:11 288 查看
1. 类
1.1 类的一些概念
(1) C++中类与结构的区别是:类定义中成员默认情况是private,而结构中是public的;
(2) C++中的结构也可以有成员函数的;
(3) ::前面可以没有类名,表示全局数据或者全局函数,在类中调用非成员函数要使用::号;::前可以有两种类型的名称,一种是名称空间的名称,一种是类的名称;
(4) 类法的调用与普通的成员函数调用不一样,类名::类方法
(5) 类的构造及析构函数没有返回类型,它们是在对象创建及失效时自动运行的;
(6) 类是一个抽象概念,它是一个不占内存的实体,因此在类的声明中不能出现赋值及对象的初始化等,如int I = 3, string s(“test”)等,这些初赋值或者初始化的工作可以放在构造函数(因为它是构造对象的所在),也可以放在类的初始化列表中;
(7) 类对象的建立是分配空间、构造结构及初始化的三位一体。
1.2 对象的创建方法
如果定义了一个类,类名为Tdate,那么它有以下几中可能的Tdate对象创建方法:
(1) Tdate d;调用Tdate的缺省构造函数;
(2) Tdate d();调用Tdate的无参数构造函数;
(3) Tdate d(参数);
(4) Tdate d = Tdate(参数);新创建一个无名对象,再把该对象赋值给d,调用了Tdate的构造及赋值函数。对类中的成员对象的初始化很有用;
(5) Tdate *d = new Tdate(参数),这时访问对象成员应用->,如d->getDate(),这种声明后最好用if(d == NULL)来判断内存分配是否成功;
(6) Tdate *d = (Tdate *) malloc(sizeof(Tdate));这种声明要手工调用它的构造函数及析构函数,并且也要用if(d == NULL)判断内存是否分配成功;
(7) Tdate m(d):用已有的指定对象创建新的指定对象,调用的是Tdate的拷贝函数;
(8) Tdate d = “2004-03-02”:如果使用这种方法,Tdate必须有一个char *类型的构造成函数,即Tdate(char * mDate),这表明可以使用字符串创建Tdate对象,这里使用了构造函数的类型转化功能。或者有一个可以由char*构造的类型做为参数,如Tdate(string date),char *可以转化成string,所以也可以转化为Tdate。最常见的构造函数类型转化就是string类的使用,如string s = “hello”;
(9) auto_ptr<Tdate> d(new Tdate(参数)):创建一个可以自动内存管理的对象,d是一个指针,它指向创建的对象;使用auto_ptr类模板需要包含memory头文件。这种定义方法的内存空间的删除需要调用类模板auto_ptr的成员方法。
(10) auto_ptr<Tdate> d1(d):根据d创建一个自动内存管理的对象,并且d中的资源的所有权转给了d1,即如果d和d1共同拥有一个堆的内存空间,即使要用delete删除也只需要delete d1。
(11) 还有一种创建方法,就是在先预分配对象的内存,然后再在该内存中真正的创建对象,这种方法需要加入new头文件,如:
class Foo{
public:
int val(){return _val;}
Foo(int i=1){ _val = i;}
private:
int _val;
};
char *buf = new char[sizeof(Foo)*2]; //预分配内存

int main()
{
Foo *p = new(buf) Foo(2); //将新创建的对象放到buf指向的内存中;
Foo *p1 = new(buf + sizeof(Foo)) Foo(3);
cout << p->val() <<":" << p1->val() << endl;
delete [] buf; //删除堆中的内存,这里并不使用delete p;
return 0;
}
程序运行后,输出的值为2:3。使用这种方法可以重复利用指定的内存空间,不需要反复的动态分配内存,已分配的内存到真正不用时再删除。
全局对象在主函数开始执行前首先被建立,局部对象在程序执行遇到它们的对象定义时才被建立。
从堆上分配对象数组,只能调用默认的构造函数,不能调用其它任何构造函数,如果类没有默认的构造函数,那么就不能分配动态的对象数组。如:
class s
{
public:
s(string name)
{
m_name = name;
}
private:
string m_name;
};
int main()
{
s *p = new s[10];
return 0;
}
上述代码使用s *p = new s[10],企图创建对象数组,但是因为类s中没有默认的构造函数,所以创建不成功。
(12) 对象另一种创建方法是:
class Test2
{
public:
unsigned int k;
unsigned int k1;
int getk()
{
return k;
}
};
Test2 k = {36,44};
这种对象初始化的方法类似于对数组的初始化,直接对k,k1这两个数据成员赋值,不过这种初始化方法有很大限制性,如不能自定义构造函数等。
(13) 对象数组的创建:
class Test
{
public:
Test(int i,int j)
{
m_i = i;
m_j = j;
}
int m_i;
int m_j;
};
Test t[2] = {Test(1,2),Test(3,4)};
1.3 类的初始化列表
类的初始化列表位于构造函数的参数列表后,函数体前,这表明在对象结构还没有建立(对象结构的建立由构造函数完成)。因此初始化列表有已以下几个特殊的作用:
(1) 初始化常量,因为常量是不能赋值,它必须在声明的同时就给它赋值,而类的声明中又不能有赋值语句,因为赋值语句会带来一系列的内存分配。因此常必须在对象结构还没有建立前初始化,所以需要放在初始化列表中进行;
(2) 引用变量的初始化,原因与第一条相同;
(3) 对象成员的初始化,如:
class StudentId
{
public:
StudentId(int id = 0)
{
value = id;
}
private:
int value;
};

class Student
{
public:
Student(int id = 1):m_id(id),m_name("jj"),m_name1(m_name)
{
}
private:
StudentId m_id;
const string m_name;
const string &m_name1;
};
当然对于成员对象的初始化也可以在构造函数中进行,如:
class Student
{
public:
Student(int id = 1):m_name("jj"),m_name1(m_name)
{
m_id = 1; //使用了构造函数的类型转换功能;
m_id = StudentId(2);//新创建一个无名对象,再赋值;
}
private:
StudentId m_id;
const string m_name;
const string &m_name1;
};
在初始化列表中不允许出现d=10这样的形式。
1.4 构造及顺序
C++规定,每个类必须有一个构造函数,如果在定义类时没有定义构造函数,那么C++会提供一个默认的构造函数,它是一个无参数的构造函数,不做初始化工作,只仅负责创建(构造)对象。只要类定义了一个构造函数(无论它是否带函数),那么C++就不会再提供这个构造函数,这时最自定义一个无参数的构造函数,否则classname obj这样的定义方法就不能再用了,而要使用classname obj(….)这样的定义方法。但是如果使用classname obj();这样的语句,在声明时不会出错,因为它定义了一个obj这样的函数,它的返回类型是classname。
局部和静态对象,以声明的顺序构造,不是以运行的顺序,而且所有变量和对象都在函数开始执行时,统一定义的,如:
int main()
{
int m = 5;
if( m == 5)
goto abc;
int n = 0;
abc:
cout << n << ',' << m << endl;
return 0;
}
以上程序有在有的编译器中是通不过的,如果通过它的执行结果为5,0。与之类似,类的成员数据以在类中也是以它们的声明顺序构造的,如:
class Test
{
public:
Test():n(2),m(n+1)
{
cout << "m:" << m << " n:" << n <<endl;
}
private:
int m;
int n;
};
创建了Test类的实例后,输出的并不是m:3 n:2,而是m:不定数 n:2,因为m的声明在n之前,所以它的构造也在n之前,因此在初始化列表中虽然n(2)在m(n+1)之前,但是实际上是先运行了m(n+1),再运行n(2)的,因此在运行m(n+1)时n还没有确定的值(与编译器有关)。
静态对象和静态变量一样,文件作用域的静态对象在主函数开始运行前全部构造完毕。块作用域中的静态对象,则在首次进入到定义该静态对象的函数时,进行构造,如:
class Test
{
public:
Test(int i)
{
cout << "class:" << i <<endl;
}
};
void f(int n)
{
static Test t(n);

cout<< "function:" << n <<endl;
}
int main()
{
f(10);
f(20);
return 0;
}
程序执行结果为:
class:10;
function:10;
function:20;
在程序的主函数中对函数f进行了两次调用,但是Test类的对象只构造了一次。即静态对象构造函数只被构造一次。
所有的全局对象在主函数开始运行之前,全部被构造(这一点与全局变量一样),因此如果这些对象的构造函数出了问题,很难调试。可以在这些构造函数中的开始加入输出语句,以定位出错的全局对象,然后再将它先作为局部对象来调试。而且由于编译器不能控制文件的连接顺序,因此多文件中的全局对象的构造顺序是不定的,因此不要在全局对象中访问另一个全局对象,因此这个被访问的全局对象可能还没被构造。如:
class student{};
class teacher{public:teacher(student &s){}};
student s1;//文件1中的全局对象
teacher t(s1);//文件2中的全局对象
可能在构造teacher t(s1)的时候s1还没被构造成。
1.5 拷贝函数赋值函数
C++提供的默认拷贝函数的工作方法是,完成一个成员一个成员的拷贝。如果成员是类对象,则调用其拷贝构造函数或者默认拷贝函数。因此对成员对象的使用也需要小心,一定要知道成员对象的拷贝函数有没有分配动态内存,如果分配了动态内存,在析构函数中有没有释放这些内存等。
自动创建的拷贝函数是浅拷贝,它是按位来拷贝的,如果一个类的方法中有堆内存的分配,那么浅拷贝使两个指针同时指向同一个资源,析构时同一个内存将被析构两次,如果这样的对象做为参数传递给函数,函数运行完毕后,实参中所在的堆资源也没了。
所以这样的情况需要重载拷贝函数。其实判断是否需要重载拷贝函数,可以看在析构函数是否使用了delete及free,如果使用了就说明在类的方法中使用了堆存,应重载拷贝函数,同样如果重载了拷贝函数一般也要重载赋值函数,重载赋值函数很简单,在调用拷贝函数之前需要判断是否是自我赋值,然后再释放自已的资源后,再调用只要调用拷贝函数即可。
重载拷贝函数的目的是实现对深拷贝,根据源拷贝对象所占的动态内存,创建相同的动态内存,然后再将拷贝对象动态内存中的数据复制到新建的动态内存中。
拷贝函数和赋值要做是将拷贝对象的成员数据的值复制过来,当然这个过程可能涉及动态内存的分配问题,仅此而已。以下是myString类的例子:
class myString
{
public:
myString();
myString(const char *str);
myString(const myString &obj);
~myString(void);
myString &operator =(const myString &obj);
const char* &c_str() //注意点0:返回值应是const
{
return m_str;
}
private:
char *m_str;
};

myString ::myString()
{
m_str = new char[2];
m_str[0] = 'a';
m_str[1] = '/0';
}
myString ::myString(const char *str)
{ //注意点1:不能使用m_str = str;
if(str == NULL)
{
//注意点2:可以使用m_str = NULL;
m_str = new char('/0');
}else{
int iLen = strlen(str);
//注意点3:strlen计算的长度不包括结束符,所以要多加一位
m_str = new char[iLen + 1];
if(m_str != NULL)
{
strcpy(m_str,str);
//注意点4:确保一定会有字符串结束符
m_str[iLen] = '/0';
}
}
}
myString::myString(const myString &obj)
{
//注意点5:可以访问对象的私有变量
int iLen = strlen(obj.m_str);
m_str = new char[iLen + 1];
if(m_str != NULL)
{
strcpy(m_str,obj.m_str);
m_str[iLen] = '/0';
}
}
myString &myString::operator =(const myString &obj)
{
if(this == &obj)
return *this;
delete [] m_str;

int iLen = strlen(obj.m_str);
m_str = new char[iLen + 1];
if(m_str != NULL)
{
strcpy(m_str,obj.m_str);
m_str[iLen] = '/0';
}
return *this;
}
myString::~myString()
{
delete [] m_str; //注意点6
}
在这个类的设计中有几个值的注意点:
(1) 注意点0:const char* &c_str(),函数c_str()返回的引用必须用const限制,不然,在对象外面使用 char *p = s.c_str(); p[0] =’a’;很容易把对象内部的私有变量的值更改了。这是不满足类的数据封装要求的。
(2) 注意点1:不能使用m_str = str,因为在注意点6中,即在析构函数中使用了delete [] m_str,即表明m_str指向的是一个动态的内存,而参数str可能指向不是动态内存,那么析构时就会出错。即使str指向一个动态内存,这样的赋值语句使用两个指针同时指向一个动态内存,析构后该内存就被释放了,此时实参的指向就很危险了。
(3) 注意点2:可以使用m_str = NULL来代替m_str = new char[‘/0’],因为对于m_str这样的定义,也可以使用delete语句,即析构时就不会出错。
(4) 注意点5:可以访问对象的私有变量,我也不知道为什么,有谁知道不妨告知8280338@tzenet.com
(5) 注意点6:在析构函数中使用了delete,所以在类的定义中必须保证所有对m_str赋值必须指向动态内存。
(6) 对于默认的构造函数myString(),还有另一种形式,即:
myString(const char *str = “hello”);//如果不提供参数的值,它就会有默认的值
但是不能用:
myString(const char *str)作为默认的构造函数
(7) 这里可以不用重载myString &myString::operator =(const char *str),因为使用myString s; s = “hello”时,进行了类型转换,而myString的构造函数实现了myString(char *),所以可以将字符串通过该构造函数转换成myString对象。
(8) 拷贝函数与赋值函数最大的区别是:拷贝函数起初始化的作用,即拷贝函数执行时如MyClass a = b时a对象还不存在;而赋值函数执行值如a=b时,a与b两个对象都已存在了。
1.6 静态成员
静态成员分为静态数据成员及静态方法成员。静态成员的声明需要加上static关键字,如:
static int num;
static int getNum(){…}
静态成员不属于类的任何具体对象,它就属于类,所以在其它语言中称静态成员为类数据成员,及类方法,这表明静态成员可以直接通过类来调用,而不需要先创建类的对象,再通过对象调用。
正因为静态成员的这些特殊性,便得静态数据成员不能在构造函数中初始化,一般静态数据成员的初始化工作在类的定义部门,以下是静态成员的定义初始化及调用的示例:
class testStatic
{
public:
static int getNumber()
{
return m_int;
}
private:
static int m_int;
};
int testStatic::m_int = 1;
int main()
{
cout << testStatic::getNumber() << endl;
return 0;
}
在这个例中使用static声明静态变量和方法,并通过int testStatic::m_int = 1初始化静态数据成员,直接使用testStatic::getNumber()来调用类的静态方法。
还有一个特别需要注意的是,在静态方法中不能访问非静态的方法及数据成员,原因很简单,静态成员在类实例化之前就存在了,可以调用了,这时非静态方法及数据成员还没有创建,所以没法调用。如果想要在静态方法中调用非静态成员需要先创建该类的实例,如:
class testStatic
{
public:
testStatic()
{
m_int1 = 10;
}
static int getNumber()
{
testStatic t;
t.m_int = t.m_int1;
return m_int;
}
int getNumber1()
{
return m_int;
}
private:
static int m_int;
int m_int1;
};
int testStatic::m_int = 1;
静态成员函数与非静态成员函数的根据区别在于静态成员函数没有this指针,而非静态成员函数有一个指向当前对象的this指针,编译时编译器会把对象的地址作为第一个参数,如上述类中int getNumber1()函数会自动加上一个对象的地址参数,变成int getNumber1(testStatic *)。
而对于静态成员函数,没有这种转换。
1.7 友员
友元的主要作用是提高效率和方便编程,但同时也破坏了类的封装。一个类的友员可以是一个普通函数,另一个类的成员函数,也可以是另一个类。友员的声明可能在类的任何部位如public,private等,以下是友员定义及使用的例子:
class test; //注意点1
class test1
{
public:
int getTest(test &t);
};
class test
{
public:
test()
{
m_value = 1;
}
friend int test1::getTest(test &t);//注意点2
private:
friend int getTest(test &t);//注意点3
int m_value;
};
int getTest(test &t)
{
return t.m_value;
}
int test1::getTest(test &t)
{
return t.m_value;
}
int main()
{
test1 t;
cout << t.getTest(test()) <<endl;
cout << getTest(test()) <<endl;
return 0;
}
这里有几个注意点:
(1) 注意点1:如果使用其它类或者其它类的方法做为友员,必然要涉及类的交叉定义,所以这里使用class test先声明类,但没有定义类test,对于只声明没有定义的类可以以有限的方式来使它,不能用它来定义对象,但是可以用它来定义指针、引用等。
(2) 注意点2:使用其它类的方法做为友元,需要加上::域操作符;
(3) 注意点3:做为友元的方法或者函数它们一般都有指定类的引用,这样才能通过该引用访问指定类对象的数据成员。
以下是个奇怪的问题,不知道为么?
#include <iostream>
using namespace std;
class Screen{
public:
Screen()
{
m_width = 100;
m_height = 200;
}
friend ostream &operator<<(ostream &o,const Screen &s);
private:
int m_width;
int m_height;
};
ostream &operator<<(ostream &o,const Screen &s)
{
o << s.m_height << '/n' << s.m_width <<endl;
return o;
}
为什么上述代码在vc++中编译通不过,提示的错误原因为不能问对象的私有变量。
但是如果把头文件变成<iostream.h>并把using namespace std去掉。或者将操作符的重载放在类的内部定义,就没这样的错误。为什么?
1.8 union类
union在c语句中也有,不过C++中的union不但可以有成员数据,还可以有成员方法。Union的最大特点是它所有的数据成员内存空间是重叠的。Union也有构造函数析构函数等,也有public、private等数据成员。
但它也有一些特殊性,如不能有静态数据成员或者引用数据成员,它的对象数据成员不能有构造函数、析构函数、拷贝函数、赋值函数。
Union类的定义及使用示例如下:
union Test
{
public:
unsigned int m_int;
char m_char;
};
int main()
{
Test t;
t.m_int = 0;
t.m_char = 'a';
cout << t.m_int << endl;
return 0;
}
输出的值是97。
1.9 位域
位域的功能非常强大,它可以将数据类型定义到bit位,当然如果只定义了一个变量,并且它的长度为1bit,实现上它占的内存空间还是1个字节(当然这根据它的定义了)。因为在类中如果可能的话,它相邻定义的位成员会被放在同一个字节的相邻位,不过个人认为还是定义为Structure的好。所以定义位变量时,一般是需要定义多个位变量,并且使它们的总长度为byte的整数倍。对位成员的访问与其它数据成员的访问是一样的。以下是位域使用的示例:
class Bit
{
public:
typedef unsigned int BIT;
BIT m_1:3;
BIT m_2:3;
BIT m_3:2;
BIT m_4:24;
};
union Test
{
public:
unsigned int m_int;
Bit b;
};
int main()
{
Test t;
t.m_int = 255;
t.b.m_1 = 0;
cout << t.m_int << endl;
return 0;
}
在使用位域的时候与union一起使用比较常用,这样对位时候的赋值就比较方便了,不用一个一个的为每个位成员赋值,只要给union成员(一般长度是所有位成员长度的和)赋值即可。
使用位域的强大功能可以方便的实现不同编码之间的转化,如binary转化为BCD,EBCDIC等。在以后的文章中我可能会用它来实现BIT、BYTE、BCD、EBCD、BITSET、BYTESET、BCDSET等类。
还有,因为位成员是以位为单位的所以它没有详细的地址,不能使用指针来访问。它也不能是类的静态成员。
1.10 局部类
局部类就是在函数体内定义的类。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: