最近在学习SLAM,顺便将C++类的知识复习一下。(其中部分官方定义和程序设计方法来源于西北工业大学魏英老师)
1.类的定义:
是用户自定义的数据类型。
C++一个类定义的形式如下:
class 类名
{
成员列表
};
成员列表是类成员的集合,数目可以任意多, 一对 { } 是成员列表边界符,与成员列表一起成为类体。类体后面必须用 ; 结束。
1.每个类可以没有成员,也可以有多个成员。
2.类成员可以是数据或函数。
3.所有成员必须在类内部声明,一旦类定义完成后,就没有任何其他方式可以再增加或减少成员。
在面向对象程序设计中,一般将变量(数据)隐蔽起来,外部不能直接访问。把成员函数作为对外界的接口,通过成员函数访问数据,可能一开始学习的时候不太理解,这个我们在后面会经常用到,请耐心观看。
类中如果有成员函数,则声明是必须的,而定义是可选的,什么意思呢,请看下例:
在类内部定义函数体
class 类名
{
返回类型 函数名(形参列表)
{
函数体
}
};
在类外部定义函数体
class 类名
{
返回类型 函数名(形参列表);
};
返回类型 类名 :: 函数名(形参列表)
{
函数体
}
看到这里会产生一个问题,那就是这两种定义方法到底有什么区别,或者根本没有区别。
其实它们还是有区别的,类内部定义的函数,程序在要调用它的时候会把它当作是一个内联函数,内联函数的好处是调用速度更快,但是会占用额外的内存空间,每调用一次都相当于定义一次。而外部定义的函数,就不会被当作内联函数。对于一些要用到递归的函数,定义成内联函数肯定是不合理的。因此建议使用第二种方法定义成员函数。
类的定义一般放在程序文件开头,或者放到头文件中被程序文件包含,当然也可以放在局部作用域里。这里有必要提一下,c++规定,在局部作用域中声明的类,成员函数必须是函数定义形式,不能是原型声明。
类相当于一种新的数据类型,数据类型不占用存储空间,用类型定义一个实体的时候,才会为它分配存储空间。
2.类成员的访问控制:
对类的成员进行访问,有两个访问源:类成员和类用户。类成员指类本身的成员函数,类用户指类外部的使用者,包括全局函数,另一个类的成员函数等。
在C++中,类的每个成员都有访问控制属性:public(公有的)、private(私有的)、protected(保护的)
类用户想要访问类的数据成员,必须通过公有成员访问。
我们上面说过,面向对象程序设计过程中一般将数据隐蔽起来,也就是说一般的变量(数据)都声明为private,而成员函数声明为public,protected在后面我们会用到,不考虑继承的话,和private的性质一致。如果在声明的时候不写访问控制属性,则类会默认它为private。
在刚才类定义的基础上进行扩展:
class 类名
{
public:
公有的数据成员和成员函数
protected:
保护的数据成员和成员函数
private:
私有的数据成员和成员函数
};
类的成员函数和普通函数一样,也可以进行重载,设置默认参数,显式的指定为内联函数等。
这里有个小问题,请看下例:
class Test { public: void Sum(int a=0,int b=0); }; void Test::Sum(int a=0,int b=0) { cout<<a+b; }
这是一个设置了默认参数的函数,但是很遗憾,这是错误的,下面这样则是正确的:class Test { public: void Sum(int a=0,int b=0); }; void Test::Sum(int a,int b) { cout<<a+b; }
原因是C++中对于特定的某个函数,设置默认形参这个动作只能有一次3.对象的定义和使用:
说了这么多,怎么样才能实现在外部实现对类成员的访问呢?这就是我们要讨论的对象。对类的定义就是定义了一个具体的数据类型,要使用它我们必须将类实例化,即定义该类的对象。
以下两种定义类对象的方法都是合法的(假定有一个Test类):
Test test1 , test2; class Test test1 , test2;
当然,有时候人们也希望可以动态的为其分配内存,当不用的时候再销毁它,就有了如下定义方式:
Test *p; p = new Test;
当不再使用此动态对象的时候,必须用delete:
delete p;
现在我们关心的应该是怎么通过对象调用类的成员?
访问对象中的成员有三种方法:
通过对象名和对象成员引用运算符 (.)
通过指向对象的指针和指针成员引用运算符 (->)
通过对象的引用变量和对象成员引用运算符 (.)
假定我们有一个Test类,类中有一个公有的Sum()函数,则在外部调用Sum()的方法有:
Test test; test.Sum(); Test *p; p = new Test; p->Sum(); Test test, &r = test; r.Sum();
4.构造函数与析构函数:
建立一个对象的时候,通常最需要做的工作就是初始化对象,如对数据成员赋初值,而构造函数就是用来在创建对象时初始化对象,为对象数据成员赋初值。为什么非得这么做呢?因为在类里面,数据成员不能够进行初始化。即:class Test { int x = 0; ... };
这样做是错误的。想想为什么不可以,还是上面说过的,类只是定义了一个数据类型,不会占用存储空间,而在类里面对数据成员赋初值则会占用存储空间,因此自相矛盾。
如果数据成员是公有的,那么可以在类外直接对它初始化,但如果是私有的,那么就不能直接访问它,这就要用到构造函数。构造函数就是用来处理对象的初始化问题,构造函数是一种特殊的成员函数,不需要人为调用,而是在对象建立的时候自动被执行。
C++规定构造函数的名字要与类名保持一致,而且不能指定返回类型。请看下面程序:
#include <iostream> using namespace std; class Test { public: Test (); Test (int x,int y); void Sum(); private: int a,b; }; Test::Test() { } Test::Test(int x,int y) { a=x; b=y; } void Test::Sum() { cout<<a+b; } int main() { Test test(3,4); test.Sum(); return 0; }
第一个为无参构造函数或默认构造函数,写这个函数的好处是当你在创建对象的时候并不想立即对它初始化,而是在后续的工作中再进行赋初值,即:
Test test;
如果没有默认构造函数则会报错。那么问题来了,之前的例子我们根本没写构造函数,却不会报错,这是为什么?因为在IDE里(ex:codeblocks)不会报错是因为IDE会自动生成一个默认构造函数。当然,如果你已经定义了一个有参的构造函数,它就不再为你自己生成一个默认构造函数,也就是说如果我们现在把这个Test类里的默认构造函数删除了,
Test test;
就会报错。第二个构造函数就完成了我们的初始化工作,它有两个形参,分别给数据成员a,b进行初始化,定义对象的时候传入了 3和4,则 a和b 被初始化为 3 和 4 。因此程序运行的结果是 打印出了 7。
构造函数初始化列表
所谓初始化列表,它的功能和我们写在函数体里的赋初值是一样的,也就是说我们可以写成如下形式:
#include <iostream> using namespace std; class Test { public: Test (); Test (int x,int y); void Sum(); private: int a,b; }; Test::Test() { } Test::Test(int x,int y):a(x),b(y) { } void Test::Sum() { cout<<a+b; } int main() { Test test(3,4); test.Sum(); return 0; }
你可以选择写的更简洁一点:#include <iostream> using namespace std; class Test { public: Test () {} Test (int x,int y):a(x),b(y) {} void Sum(); private: int a,b; }; void Test::Sum() { cout<<a+b; } int main() { Test test(3,4); test.Sum(); return 0; }
那么这样做和普通的赋值有区别吗?当然是有的,对于一般的变量,两种都可行,但是如果需要初始化的是类类型的成员,则必须使用构造函数初始化列表。比如:
#include <iostream> using namespace std; class Test { public: Test () {} Test (int x,int y):a(x),b(y) {} void Sum(); private: int a,b; }; void Test::Sum() { cout<<a+b; } class AnotherTest { public: AnotherTest(int i,int j):test(i,j) {test.Sum();} private: Test test; }; int main() { AnotherTest test(3,4); return 0; }
构造函数也是可以的,刚才那个例子就是构造函数的重载,默认构造和有参构造。
下面是一个带默认参数的构造:
#include <iostream> using namespace std; class Test { public: Test () {} Test (int x = 0,int y = 0):a(x),b(y) {} void Sum(); private: int a,b; }; void Test::Sum() { cout<<a+b; } int main() { Test test(3); test.Sum(); return 0; }
Test (int x = 0,int y):a(x),b(y) {}
这样是错误的。复制构造函数
复制构造函数也称为拷贝构造函数,它的作用是用一个已经生成的对象来初始化另一个同类的对象。
即实现如下功能:
Test test1(3,4); Test test2 = test1;
复制构造函数的写法:类名 (const 类名& obj)
{
函数体
}
例如:
#include <iostream> using namespace std; class Test { public: Test () {} Test (int x ,int y):a(x),b(y) {} Test (const Test& t):a(t.a),b(t.b) {} void Sum(); private: int a,b; }; void Test::Sum() { cout<<a+b; } int main() { Test test1(3,4); Test test2 = test1; test2.Sum(); return 0; }
当然也可以用如下语句:
Test test2(test1);
深复制和浅复制:如果我们不定义复制构造函数,以上对象也可以这样进行初始化,原因就是系统也会自己生成一个复制构造函数。
现在存在这样一个类:
#include <iostream> #include <cstring> using namespace std; class Test { public: Test (int x,char *ptr) { a = x; p = new char [x]; strcpy(p,ptr); } Test (const Test& C) { a = C.a; p = new char [a]; p = C.p; } void Print(); private: int a; char *p; }; void Test::Print() { int i = 0; while(p[i] != '