c++类和对象以及模板
类和对象
在讨论类和对象时,需要重点理解STL库中的容器以及面向对象的三大特性:封装、继承和多态。
类的特性主要体现在其整体性上。与外部函数严格要求的调用顺序不同,类域作为一个整体,其成员函数和成员变量可以在任何位置被识别,无需考虑调用顺序问题。封装通过将对象的属性和方法相结合来实现,使对象更加完善。通过访问限定符(public、private、protect)可以有选择地向外部用户提供接口。其中:
- public:公开访问
- private和protect:受限访问(具体区别将在继承和多态中详述)
访问权限的作用域从当前限定符开始,直到下一个限定符为止。值得注意的是:
- class默认访问权限为private
- struct默认访问权限为public(保持与C语言兼容)
- 类内定义的函数默认为内联函数
封装的核心在于将数据与操作方法有机结合,隐藏对象属性和实现细节,仅对外公开交互接口。这种管理方式让类的使用更加便捷。对象大小仅包含成员变量,不包括成员函数。空对象占用1字节空间,仅用于标识对象存在。
关于this指针:
- 不能显式传递于形参和实参
- 可在函数内部显式使用
- 作为形参存储在函数调用栈帧中
- C++与C的实现本质相同,区别在于C++自动处理指针传递
类的六个默认成员函数:
构造函数(初始化)特性:
- 与类同名
- 无返回值(包括void)
- 对象实例化时自动调用
- 支持重载
- 未显式定义时,编译器生成默认构造函数
析构函数(清理)特性:
- 名称格式:~类名
- 无参数和返回类型
- 不可重载(每个类唯一)
- 对象生命周期结束时自动调用
- 未显式定义时,编译器生成默认析构函数
对于包含自定义类型的类:
- 构造时会递归调用自定义类型的构造函数
- 析构时先释放自身再释放自定义类型成员(避免非法访问)
拷贝构造:
- 拷贝构造是构造函数的一个重要重载形式
- 拷贝构造函数的参数只能有一个,而且必须是类类型对象的引用(若使用传值会直接报错,因为会引发无穷递归调用)
- 未显示定义时,编译器生成默认拷贝构造函数(默认的会进行值拷贝(浅拷贝),自定义类型会去调用它的赋值重载)
对于构造函数的初始化,有不同的方式,我简单用代码构建一下
class test{ public: test(int & t,int & k) : val(t),key(k){} test(int t) { val = t; } private: int val; int key; };看了这两个构造方式,想必大家也理解了,第二个就和正常函数一样,而第一个用的是成员初始化列表,而:代表初始化列表开始,初始化方式用的是(),两个初始化成员变量之间用,隔开。
还要提及构造函数的一个特点,他是支持隐式类型转换的
class test{ public: test(int t) : val(t){} private: int val; }; int main() { test t = 1; return 0; }像这样,整形转化为自定义类型,此处的1会构造一个test的临时对象,再拷贝构造t,但是部分编译器会做优化,将其转变为用1直接构造test对象。如果想禁止这种行为,可以使用关键字explicit,会禁止隐式类型转换。
静态成员变量:
- 在类里声明,在全局定义
- 属于类,类的每个对象共享一个静态成员变量,且存储在静态区
静态成员函数:
- 没有了this指针,访问时只需指定类域和访问限定符即可
这两个一般成对出现。
友元类:
- 内部类是外部类的天生友元
- 使用friendly对其他类进行声明,说明其他类是该类的友元类
对象:
普通对象就像上面的test t,所以我们讲特殊的,匿名对象和临时对象,这两个对象都有一个共同之处,都具有常性。
匿名对象:
- 生命周期只在当前语句行
- 具有常性
- const引用的匿名对象会延长自己的生命周期,生命周期在当前局部域
内存操作符:
也说一下创建对象常用的操作符new,它和delete配对,如
class test{ public: test(int t) : val(t){} private: int val; }; int main() { test* t = new test(1); delete t; return 0; }和malloc,free一样,他们也是一组关于内存开辟释放的操作符,有new一定要delete,否则就会造成内存泄漏。
模板与模板特化
说到模板,首先要清楚模板的关键词template,下面简单写一个模板
template<class T,class K> // <typename T> class test{ public: test(T& t,K& k) : val(t),key(k){} private: T val; K key; };看到这段代码肯定会困惑,template后面那个是做什么用的,其实用typename会更好理解,他就是代表一个类型,至于是什么类型,看你传的参数是什么类型而自动推导的,但是实际上,模板做的工作是以你传入的类型重新构建一份代码,每次传递的不一样,都会额外生成一个,那如果有一些类型需要比正常的多出一些功能怎么办?又或者处理方式有差异怎么办?那就得提到模板的特化了,特化有两种,一个是全特化,一个是偏特化,我分别写出来,看完大家想必也会有一些自己的理解了。
全特化:
#include <iostream> // 通用模板 template <typename T> class MyTemplate { public: void print() { std::cout << "普通模板" << std::endl; } }; // 全特化版本(int 类型) template <> class MyTemplate<int> { public: void print() { std::cout << "int全特化模板" << std::endl; } }; int main() { MyTemplate<double> obj1; MyTemplate<int> obj2; obj1.print(); // 输出: 普通模板 obj2.print(); // 输出: int全特化模板 }偏特化:
#include <iostream> // 通用模板 template <typename T> class MyTemplate { public: void print() { std::cout << "普通模板" << std::endl; } }; // 偏特化版本(指针类型) template <typename T> class MyTemplate<T*> { public: void print() { std::cout << "指针模板" << std::endl; } }; int main() { MyTemplate<double> obj1; MyTemplate<int*> obj2; obj1.print(); // 输出: 普通模板 obj2.print(); // 输出: 指针模板 }显而易见,全特化,是指把他完全特化为具体的类型,偏特化是对部分模板参数进行特化,而不是具体的类型。如果上面有误,希望点出,你我共勉,一同进步。
