C的过渡
引用类型
1
2
3
4
|
int c, d;
int &a = c;
const int &b = c;
b = d;
|
常量引用类型不是该变量的值不能改变,而是它无法改变指向的变量的值(同样的,常量指针类型变量也是如此)。
1
2
3
4
5
|
int a, b;
const int *p;
p = &a;
p = &b;//合法
*p = 666;//不合法
|
特别注意,引用类型声明的时候必须初始化,单纯int &a;
是非法的。
还需记忆一下类型转换:
T
可以隐性转化为const T
const T
不可以给T
类型赋值,但是可以通过(T)强制类型转换来实现
1
2
3
4
5
6
7
8
|
const int a = 114;
int &b = (int&)a;
cout << b;
const int a = 514;
int *b = (int*) &a;
cout << *b;
//若去掉强制类型转换则无法通过编译
|
此外还需特别提及一下指针的引用
1
2
|
int *a, *b, *c;//虽然我们知道此指针的类型是int *,但是为什么SB的c还得每个变量前都加*呢?
void function(int* &a, int* &b){}//出现了一个抽象的顺序,引用指针竟然不是&*反而是*&,这也恰恰证明int *确确实实是一个type,所以指针的引用是这么写滴。
|
顺便搬运一下几种指针看法:
“ * ”的优先级低于“ ( )”的优先级
int *a;
你只需要看右边,*a 一定是一个 int 类型。
int (*a)(int);
你把 (*a) 看做一个整体,就知道,(*a) 一定是一个返回 int,接受一个 int 参数的函数,所以 a 是函数指针。
int *a[5];
*a[i] 是 int,所以 a[i] 一定是 int*,所以 a 是一个数组,数组里的每一个元素都是 int*。
int (*a)[5];
(*a)[i] 是 int,所以 (*a) 是一个 int 数组,a 是指向数组的指针。
int (*a[5])(int);
(*a[i]) 是一个返回 int,接受一个 int 参数的函数,所以 a[i] 是一个函数指针,所以 a 是函数指针的数组。
注:使用当函数指针传参的时候需要函数名和参数表一起传入。
如:
1
2
3
|
ostream& operator<<(ostream& (*func)(ostream&)) {
return func(*this);
}
|
动态内存分配
1
2
3
4
5
6
7
|
T *p;
//分配变量
p = new T;
delete P;
//分配数组
p = new T[N];
delete [] p;
|
我们通过二维动态数组的分配来加深理解:
对于指针p
,p[i]
等价于*(p + i)
。
1
2
3
4
5
6
7
8
9
10
|
int r, c;
int **p = new int[r];
for(int i = 0; i < r; i++) {
p[i] = new int[c];
}
//do something
for(int i = 0; i < r; i++) {
delete [] p[i];
}
delete [] p;
|
函数
内联函数
在函数前加上inline
,当频繁调用一个代码量较少的函数时,可以通过替换代码,减少函数栈的使用,加快速度。
注意:
- 关键字inline 必须与函数定义体放在一起才能使函数成为内联,声明时的inline不起作用。
- 内联函数本身不能是直接递归函数。
重载函数
当函数名称相同时,参数表不同时,编译器可以自动判断参数类型来实现调用特定函数。
注意:名称和参数表相同,但返回值类型不同的函数是重复函数,而非重载函数,是错误的。
缺省参数
英文很好理解就是default
当传参小于需要的参数个数时,函数的默认参数
1
2
3
4
5
6
|
void Example(int a, int b = 5, int c = 6){}
Example(1, 2, 3);
Example(1, 2);
Example(1);
//注意只能最右面的连续若干个参数缺省
Example(1,,3);//这是非法的
|
函数作为参数
作为后文的补充内容。
我们先讨论以下函数。
1
|
return_type function_name (parameters){}
|
我们初学的时候觉得这样的定义理所当然,但function_name
的类型到底是什么呢?它存储的是一个地址跟前面的返回类型没什么关系。所以*function_name
才是所谓的函数,平时我们调用的时候也从不写*,因为你不能直接调用一个函数,你需要通过一个指针来调用它。
于是进一步学习函数指针。
1
|
type (*f)(parameters);//这是一个常见的声明
|
想要调用函数,我们需要明确这个函数需要哪些参数,也就是说参数表是函数的一个属性,是它作为辨识的一个特点。所以函数指针的类型是包括参数表的。形如:type* (parameters)
。至于为什么是(*f)
,虽说我们知道指针的类型是type*
,但是在(1)括号先于*(2)在 C++ 中,函数的声明和定义的顺序是从右到左的,为了避免函数名和参数表先结合成为返回值为指针的函数,括号是必要的。
有了以上只是作为铺垫,函数作为参数就很容易理解了。将函数指针声明写在函数的参数表里即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <iostream>
using namespace std;
void Foreach(int* begin, int* end, void (*f)(int)) {
for (int* p = begin; p != end; ++p) {
f(*p);
}
}
//注意:在参数表声明使用auto仅在c++20标准支持,所以void (*f)(int),也可以写作auto f
void Print(int s) {
cout << s << " ";
}
int a[100];
int main() {
int n;
cin >> n;
for (int j = 0; j < n; ++j)
cin >> a[j];
Foreach(a, a + n, Print);
cout << endl;
return 0;
}
|
但是这样还是太烦琐和局限了,我们可以通过模板来实现传入各种各样的函数指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <iostream>
using namespace std;
template <typename T>
void Foreach(int* begin, int* end, T f) {
for (int* p = begin; p != end; ++p) {
f(*p, *p + 1);
}
}
void Print2(int s, int b) {
cout << s << " " << b << " ";
}
int a[100];
int main() {
int n;
cin >> n;
for (int j = 0; j < n; ++j)
cin >> a[j];
Foreach(a, a + n, Print2);
cout << endl;
return 0;
}
|
这样也绝非完美,参数的个数还是收到限制的,即使函数有缺省值,你也必须提供完整参数,否则无法编译通过。想要解决这个问题我们需要使用lamada
表达式作为函数参数。
类和对象
基本概念
1
2
3
4
5
6
7
8
9
10
|
class 类名 {
public:
公用的成员变量和成员函数
protected:
保护的成员变量和成员函数
private:
私有的成员变量和成员函数
};
//pubic\private\protected出现的次数和先后顺序没有限制
//若没有上述关键词,则被缺省地认为是private
|
- public:可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问;
- protected:可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问;
- private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象、访问。
类的成员:变量,函数,对象
类是当结构化程序实现功能时过于繁琐时的替代产物,相当于把一类事物抽♂象出来,每一类事物一定有它的属性,和它的各种行为,我们将其用成员变量和成员函数来表达这一类事物。
对象是根据类来定义的,通过类名 对象名;
来声明。
类的函数和之前提及的函数一样,也可以实现重载,缺省,它的声明在类里,但是它的定义可以在类外实现,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
using namespace std;
class Ex {
public:
double a,b,c;
void output();
private:
int a_function();
};
int Ex::a_function(int x, int y){do sth};
void Ex::output() {
cout << a << " " << b << " " << c << endl;
//和在类里面一样访问成员变量
}
|
此外类类似于struct也可以通过X.x
和指针p -> x
进行访问。
关于初始化:
在 C++ 中,你可以在类的定义中为非静态成员变量提供一个默认的初始化器。这是一个例子:
1
2
3
4
|
class MyClass {
public:
int flag = 1;
};
|
在这个例子中,每当创建一个 MyClass
的实例时,成员变量 flag
都会被初始化为 1
。
请注意,这个特性在 C++11 及以后的版本中可用。如果你使用的是 C++11 之前的版本,你需要在类的所有构造函数中初始化 flag
。例如:
1
2
3
4
5
6
|
class MyClass {
public:
int flag;
MyClass() : flag(1) {}
};
|
在这个例子中,flag
在 MyClass
的构造函数的初始化列表中被初始化为 1
。
构造函数
你可知道构造函数的N种类型!(移动构造,委托构造std=c++11 to be continued)
和类名相同并且无任何返回的函数。(无任何返回不是返回void)
默认生成无参数的(复制)构造函数。
构造函数:类名 (参数表)
。
复制构造函数:类名 (类名(常)引用类型)
。
(显式)类型转换构造函数:(explicit) 类名 (单个参数)
。注:尽量使用显式,防止意外之喜。此外显式和隐式类似于原生的类型转换。
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
using namespace std;
class Ex {
public:
int a, b, c;
Ex(int _a, int _b, int _c);
void output();
Ex(const Ex &k) {
a = k.a;
b = k.b;
c = k.c;
cout << "copy" << endl;
}
Ex(int k) {
a = k;
b = k + 1;
c = k + 2;
cout << "transform" << endl;
}
};
Ex::Ex(int _a, int _b, int _c = 6) {
a = _a;
b = _b;
c = _c;
}
void Ex::output() {
cout << a << " " << b << " " << c << endl;
}
int main(){
Ex a(1, 2, 3);
a.output();
Ex b(5,6);//缺省构造
b.output();
Ex c = 6;
Ex d(7);//类型转换构造
}
|
调用复制构造函数的三种情况
- 初始化时,以下两种方式等价
Ex a;Ex b(a);
和Ex a; Ex b = a;
.
- 传形参,传入函数的参数为类,且不是引用类型时
- 函数返回类型为类
注意,对象之间赋值不调用构造函数,只有初始化的时候才调用。
Ex a, b; b = a
该条语句就不会调用。
最后,有以上第2条可知,传参的时候存在开销,所以可以采用引用类型进而不调用复制构造函数,若确保原变量不被改变,则传入参数为常引用类型。
此处应有一道例题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#include <iostream>
using namespace std;
class Sample {
public:
int v;
Sample(int n) { v = n; }
Sample() { v = 0; }
Sample(Sample& b) { v = 2 + b.v; }
void PrintAndDouble();
};
void PrintAndDouble(Sample o)
{
cout << o.v;
cout << endl;
}
int main()
{
Sample a(5);
Sample b = a;
cout << b.v << endl;
PrintAndDouble(b);
Sample c = 20;
PrintAndDouble(c);
Sample d;
d = a;
cout << d.v;
return 0;
}
/**
output:
7
9
22
5
**/
|
关于复制构造函数在不同编译器上的情况:
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#include <iostream>
using namespace std;
class A {
public:
int x;
A(int x_) : x(x_) {
cout << x << " constructor called" << endl;
}
A(const A &a) {
x = 2 + a.x;
cout << "copy called" << endl;
}
~A() { cout << x << " destructor called" << endl; }
};
A f() {
A b(10);
return b;
}
int main() {
A a(1);
a = f();
return 0;
}
|
在g++上(进行了优化):
1 constructor called
10 constructor called
10 destructor called
10 destructor called
在vs的cl上:
1 constructor called
10 constructor called
copy called
10 destructor called
12 destructor called
12 destructor called
析构函数
写法:~ 类名(){}
对象消亡时调用。
注意:new
出来的对象不delete
就不会消亡
this指针
this指针的存在我们可以理解为,早期编译过程C++ – > C –>机器码,而class的存在无疑和c中的struct的相似,而c的struct没有实现类似成员函数的功能,所以函数只能写在struct外,进而为了知道我们修改的是哪一个成员,我们便需要一个this指针:其作用就是指向成员函数所作用的对象 。
以下是一份C++代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#include <iostream>
using namespace std;
class Car {
public:
int price;
void SetPrice(int p);
};
void Car::SetPrice(int p) {
price = p;
}
int main() {
Car car;
car.SetPrice(20000);
return 0;
}
|
翻译成C语言则如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#include <stdio.h>
typedef struct {
int price;
} Car;
void SetPrice(Car* this, int p) {
this -> price = p;
}
int main() {
Car car;
SetPrice(&car, 20000);
return 0;
}
|
非静态成员函数中可以直接使用this来代表指向该函数作用的对象的指针。而静态成员函数没有对应的对象,所以无法用this调用。但是调用的函数一定不能访问成员才有的值,否则会报错。
进而我们可知,非静态成员函数的参数比已知的隐性多一个,而静态成员函数的参数就是你写上去的那些。
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <iostream>
using namespace std;
class A {
public:
int i;
void Hello() { cout << "hello" << endl; }
void crash() { cout << i << endl; }
};
int main() {
A* p = NULL;
p->Hello();
p->crash();//crash
}
|
注意:对于一个想要改变对象内成员变量的成员函数还非得有返回值的函数要返回引用!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
IntSet& IntSet::insert(int e)
{
if (!is_element(e)) {
Node* p = new Node;
p->num = e;
p->next = head;
head = p;
len++;
}
return *this;
}
IntSet s;
for (int i = 0; i < 6; i++) s.insert(i);
//因为返回引用所以可以实现
s.insert(114).insert(514);
|
此时返回IntSet会析构又复制构造,乱七八糟的就报错了。
静态成员
类的静态变量类似于全局变量,namespace为类。又因为它时共用的所以不占用单个对象的内存,sizeof(类)
不包括其大小
类的静态成员函数没有this
指针,只能访问静态成员。
静态成员,所有对象都可以对其进行访问,静态成员只要使用类名加范围解析运算符 :: 就可以访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
#include <iostream>
#include <string>
class Person {
private:
std::string name;
int age;
public:
static int personCount;
static void displayCount() {
std::cout << "Person count: " << personCount << std::endl;
}
Person(std::string name, int age) : name(name), age(age) {
personCount++;
}
Person(const Person& person) {
name = person.name;
age = person.age;
personCount++;
}//对复制进行重载,不遗漏情况
~Person() {
personCount--;
}
void displayInfo() {
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
}
};
int Person::personCount = 0;
int main() {
Person person1("John Doe", 25);
Person person2("CBK", 24);
std::cout << Person::personCount << std::endl;
Person::displayCount();
return 0;
}
|
注意:静态成员变量一定要进行初始化,而ISO C++禁止在类的内部初始化非常量,所以不要忘记在类的外部初始化。
对静态成员变量时候操作时候,考虑情况要做到不遗漏,考虑构造/析构时的各种情况。
成员对象
成员对象,就是是对象的成员(废话),就是套娃。有成员对象的类就是封闭类。//定义存疑找不到
任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。
与此同时,我们引入C++ 类构造函数的初始化列表。
1
|
类名::构造函数名(参数表): 成员变量1(参数表), 成员变量2(参数表), ...{}
|
初始化数据成员与对数据成员赋值的含义是什么?有什么区别?
首先把数据成员按类型分类并分情况说明:
1.内置数据类型,复合类型(指针,引用)
在成员初始化列表和构造函数体内进行,在性能和结果上都是一样的
2.用户定义类型(类类型)
结果上相同,但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体前已经构造完成,也就是说在成员初始化列表处进行构造对象的工作,调用构造函数,在进入函数体之后,进行的是对已经构造好的类对象的赋值,又调用个拷贝赋值操作符才能完成(如果并未提供,则使用编译器提供的默认按成员赋值行为)
有的时候必须用带有初始化列表的构造函数:
- 1.成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
- 2.const 成员或引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值。
引用
封闭类构造函数和析构函数的执行顺序 :
- 封闭类对象生成时,先执行所有对象成员的构造函数,然后才执行封闭类的构造函数。
- 对象成员的构造函数调用次序和对象成员在类中的说明次序一致,与它们在成员初始化列表中出现的次序无关。
- 当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数。次序和构造函数的调用次序相反。
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#include <iostream>
using namespace std;
class Tyre { //轮胎类
private:
int radius; //半径
int width; //宽度
public:
Tyre(int r, int w) :radius(r), width(w) {}
};
class Engine {};//发动机类
class Car { //汽车类
private:
int price; //价格
Tyre tyre;
Engine engine;
public:
Car(int p, int tr, int tw):price(p), tyre(tr, tw){};
};
int main() {
Car car(20000, 17, 225);
return 0;
}
|
上例中,如果 Car
类不定义构造函数, 则Car car;
会编译出错:
因为编译器不明白 car.tyre
该如何初始化。 car.engine
的初始化没问题,用默认构造函数即可 。
友元
友元分为友元函数和友元类两种
友元函数: 一个类的友元函数可以访问该类的私有成员 ,可以将一个类的成员函数(包括构造、析构函数)说明为另一个类的友元。(该函数也可以不是任何类的成员函数)(友元函数无this指针)(友元函数即使是全局函数也可以写在类里,且参数个数正常)
友元类: 如果A是B的友元类,那么A的成员函数可以访问B的私有成员。
友元类之间的关系不能传递,不能继承。
友元函数
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#include <iostream>
using namespace std;
class CCar;
class CDriver {
public:
void ModifyCar(CCar* pCar); //改装汽车
};
class CCar {
private:
int price;
friend int MostExpensiveCar(CCar cars[], int total); //声明友元
friend void CDriver::ModifyCar(CCar* pCar); //声明友元
};
void CDriver::ModifyCar(CCar* pCar) {
pCar->price += 1000; //汽车改装后价值增加
}
int MostExpensiveCar(CCar cars[], int total) {
int tmpMax = -1;
for (int i = 0;i < total; ++i)
if (cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
class B {
public:
void function();
};
class A {
friend void B::function();
};
int main() {
return 0;
}
|
友元类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class CCar {
private:
int price;
friend class CDriver; //声明CDriver为友元类
};
class CDriver {
public:
CCar myCar;
void ModifyCar() {//改装汽车
myCar.price += 1000;//因CDriver是CCar的友元类,
//故此处可以访问其私有成员
}
};
int main(){ return 0; }
|
常量成员函数
对于常量对象只能使用构造函数、析构函数和 有const 说明的函数(常量方法)
在类的成员函数说明后面可以加const关键字,则该成员函数成为常量成员函数。 常量成员函数内部不能改变属性的值,也不能调用非常量成员函数。即该函数对于成员变量,只可读不可写。
注意:
- 在定义常量成员函数和声明常量成员函数时都应该使用const 关键字。
- 成员变量前有
mutable
,则在常量成员函数中,也可修改成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class Sample {
private :
int value;
mutable int value2;
public:
void PrintValue() const;
};
void Sample::PrintValue() const { //此处不使用const会导致编译出错
value2 ++;//合法
cout << value;
}
void Print(const Sample & o) {
o.PrintValue(); //若 PrintValue非const则编译错
}
|
常量成员函数的重载 :两个函数,名字和参数表都一样,但是一个是const,一个不是,算重载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#include <iostream>
using namespace std;
class Sample {
private :
int value;
public:
void PrintValue() const;
void PrintValue();
Sample(int v) : value(v) {}
};
void Sample::PrintValue() const {
cout << value << endl;
}
void Sample::PrintValue() {
cout << value * 2 << endl;
}
int main() {
Sample o(114);
const Sample co(514);
o.PrintValue();
co.PrintValue();
return 0;
}
|
运算符重载
形式为:类型 operator 运算符 (参数表) (const可选){}
。
进行重载的时候要考虑好返回类型和基本逻辑,如是否满足交换,连续运算,链式赋值等情况。
运算符重载为成员函数时,隐含this指针。
例:
1
2
3
4
5
6
7
8
9
|
//假设有Complex类,包含 real imag
Complex operator + ( const Complex & a, const Complex & b) {
return Complex( a.real+b.real,a.imag+b.imag); //返回一个临时对象
}
Complex Complex::operator - (const Complex & c) {
return Complex(real - c.real, imag - c.imag); //返回一个临时对象
}//Complex a, b, c;
//c = a + b; 等价于c = operator + (a, b);
//a - b 等价于a.operator - (b)
|
=
只能重载为成员函数。
下面是不可重载的运算符:
.
:成员访问运算符
.*
, ->*
:成员指针访问运算符
::
:域运算符
sizeof
?
::条件运算符
#
: 预处理符号
我们通过实现简易string
类和complex
类来对其加深理解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
#include <iostream>
#include <cstring>
using namespace std;
class String {
char* pstr;
public:
friend ostream& operator<<(ostream& os, const String& s);
String() : pstr(new char[1]) { pstr[0] = 0; }
String(const char* h) {
int len = strlen(h);
pstr = new char[len + 1];
strcpy(pstr, h);
}
String(const String& s) {
int len = strlen(s.pstr);
pstr = new char[len + 1];
strcpy(pstr, s.pstr);
}//自定义的赋值构造函数具有其存在必要性,否则当初始化出现=时,是浅复制,可能导致delete两次相同内存。
String& operator=(const String& other) {
if (this != &other) {//需防止自己等于自己时产生bug
delete[] pstr;
int len = strlen(other.pstr);
pstr = new char[len + 1];
strcpy(pstr, other.pstr);
}
return *this;
}
String& operator = (const char* c) {
if (pstr != c) {
int len = strlen(c);
delete[] pstr;
pstr = new char[len + 1];
strcpy(pstr, c);
return *this;
}
return *this;
}
~String() {
delete[] pstr;
}
};
ostream& operator << (ostream& os, const String& s) {
os << s.pstr;
return os;
}
int main() {
char cbk[] = "abc";
String a(cbk);
String b(a);
String c;
c = a;//在 C++11 及以后的版本中,如果你定义了一个拷贝构造函数但没有定义一个拷贝赋值运算符,编译器会生成一个警告。
cout << c << endl;
//即cout.operator << (c) << endl;
return 0;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#include <iostream>
#include <cstdio>
using namespace std;
class complex {
private:
double real, imag;
public:
friend ostream& operator << (ostream& os, const complex& c);
complex(double r = 0, double i = 0) : real(r), imag(i) {}
complex operator + (const complex& c2) const {
return complex(real + c2.real, imag + c2.imag);
};
complex operator - (const complex& c2) const {
return complex(real - c2.real, imag - c2.imag);
};
bool operator == (const complex& c2) const {
return real == c2.real && imag == c2.imag;
};
bool operator > (const complex& c2) const {//复数无法比较大小,只是比较模长
return real * real + imag * imag > c2.real * c2.real + c2.imag * c2.imag;
};
};
ostream& operator << (ostream& os, const complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
int main() {
complex c1(1, 2), c2(3, 4);
cout << c1 << c2;
return 0;
}
|
注:
- 类型强制转换运算符被重载时不能写返回值类型,实际上其返回值类型就是该类型强制转换运算符代表的类型
- 全局函数有时需要友元来访问对象私有属性,虽然很破坏封装性
- 注意自增自减运算符的前置和后置,如在成员函数内,前置
T & operator++();
,后置T operator++(int);
此处的int不代表类型,只是为了向编译器说明这是一个后缀形式,而不是表示整数。
对于可变长度的数组可以利用到[]的重载,核心在于int&类型的返回是的a[i] = k
和k = a[i]
均能成立。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
class IntArray {
private:
int* data; // 指向动态数组的指针
int size; // 数组的大小
public:
// 构造函数
IntArray(int size) : size(size) {
data = new int[size];
}
// 拷贝构造函数
IntArray(const IntArray& other) : size(other.size) {
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = other.data[i];
}
}
// 拷贝赋值运算符
IntArray& operator=(const IntArray& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = other.data[i];
}
}
return *this;
}
// 析构函数
~IntArray() {
delete[] data;
}
// 获取数组元素
int& operator[](int index) {
return data[index];
}
// 获取数组大小
int getSize() const {
return size;
}
};//本代码由ai生成,供参考
|
特别神奇的备注:对于二维动态数组数组,[]
的返回类型是int*
。
如int *operator[](int i) { return p[i]; }
,这是由于[]
的顺序是从左到右的,然后第二次进行运算时不进行重构!
继承和派生
C++类之间存在四种关系(old school):
- 没有关系(乐
- 继承关系:A,B两个类,B 是 A,B就可以通过继承A,成为A的派生类,减少代码重复。如男人和人之间的关系。
- 复合关系:前文的封闭类一样,类中有类成员
- 委托关系:类中包含其他类的指针。两个类可以通过互相委托,但是无法互相复合。
对于继承的设计要符合逻辑,判断B到底是不是A。只是包含的关系应采用复合关系。
继承
基本写法:class derived-class: access-specifier base-class
访问 |
public |
protected |
private |
同一个类 |
yes |
yes |
yes |
派生类 |
yes |
yes |
no |
外部的类 |
yes |
no |
no |
这里我们可以知道protected 的范围比 private大一点点。
- protected继承时,基类的public成员和protected成员成为派生类的protected成员,private不可访问。
- private继承时,public, protected变成派生类的private成员,private不可访问,对于该派生类的派生类,无法访问基类继承过来的任何东西,起到了阻断的作用。
此外派生类的成员函数中也可以访问非this
对象的protected
成员。但是注意无法修改基类的protected
成员,只能修改派生类从基类继承过来的成员,如下面例子中的参数只能是B b
而不能是是A a
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <iostream>
using namespace std;
class A {
protected:
int x;
};
class B: public A {
public:
void set_print(B b, int i) {
b.x = i;
cout << b.x << endl;
}
};
int main() {
B b;
b.set_print(b, 10);
return 0;
}
|
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
继承可以继承很多次,派生类具有直接基类和间接基类,我们只需要列出它的直接基类就会自动向上继承它的间接基类
派生类的成员包括自己,直接和间接基类全部成员。
例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <iostream>
#include <string>
using namespace std;
class Student {
protected:
string name;
int age;
int gender;
public:
void show_info() {
cout << "name: " << name << "age:" << age << endl;
}
};
class Undergraduate : public Student {
int score;
public:
void show_info() {
Student::show_info();//调用父类的show_info,已经实现一部分功能
cout << "score: " << score << endl;
}
};
|
虚基类
对于菱形继承关系:A->B, A->C, B,C->D。在构造D对象的时候会出现两次构造基类A的情况,这是错误的!所以A应该作为B, C的虚基类,这样A的构造函数将由最后的派生类的构造函数中构造。
1
2
3
4
5
6
7
8
9
|
class A{
}
class B : virtual public A {
}
class C : virtual public A {
}
class D : public B, public C {
}
|
构造和析构
派生类的内存大小 = (基类 + 自己)的内存大小
构造和析构的顺序类似于封闭类,在创建派生类的对象时:
-
最先执行虚基类的构造函数
-
先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;
-
再执行成员对象类的构造函数,用以初始化派生类对象中成员对象。
-
最后执行派生类自己的构造函数
消亡时:
- 先执行派生类自己的析构函数
- 再依次执行各成员对象类的析构函数
- 最后执行基类的析构函数
- 最最后执行虚基类的析构函数
如何正确地构造派生类呢?
就以CT(counter terrorist)为例子,通过列表初始化调用基类地构造函数,这样就可以避免私有成员地无法访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Person {
int health, money;
public:
Person(int h, int m) : health(h), money(m) {}
};
class equipment {
int weapon, item;
public:
equipment(int w, int i) : weapon(w), item(i) {}
};
class CT : public Person {
equipment eq;
public:
CT(int h, int m, int w, int i) : Person(h, m), eq(w, i) {}
};
|
此外还要了解虚析构函数,在后文有提及。
覆盖
派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。要在派生类中访问由基类定义的同名成员时,要使用作用域符号::
。
public赋值情况
1
2
3
4
|
class base { };
class derived : public base { };
base b;
derived d;
|
- 派生类的对象可以赋值给基类对象
b = d;
- 派生类对象可以初始化基类引用
base & br = d;
- 派生类对象的地址可以赋值给基类指针
base * pb = & d;
注意:如果派生类型是private
或protected
则不可以。
多继承
已经属于抽象的是了,但对于一个对象B确实可以是A也可以是C,正如在学校是学生,在家时孩子一样。
1
2
3
4
|
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
|
复合和委托
经典案例:
1
2
3
4
5
6
7
8
9
10
|
class CPoint {
double x, y; //点的坐标
};
class CCircle: public CPoint{
double radius; //半径
};//#1
class CCircle {
CPoint center; //圆心
double radius; //半径
};//#2
|
对于#1的继承,看似省事,但是是不符合逻辑的,只是功能上的实现,#2才是合理的写法。
然后就是提及到的小区人狗管理系统。我们既需要知道主人有哪几条狗,也需要知道狗的主人是谁。
但是如果我们尝试用复合关系去写呢?我们就会发现出现循环定义的问题。
1
2
3
4
5
6
7
|
class CDog;
class CMaster {
CDog dogs[10];
};
class CDog {
CMaster m;
};
|
当我们尝试计算sizeof(CDog)
就可以显然发现这个错误。然后我们进行第一次修改。
1
2
3
4
5
6
7
|
class CDog;
class CMaster {
CDog* dogs[10];
};
class CDog {
CMaster m;
};
|
这此无疑可以通过编译,但是对于好几条同一个主人的狗,他们的m意义是相同的,但是内存是不同的,当我们对主人进行操作时,状态无法对这么多狗轻松进行同步,所以还是不合理的。我们进行第二次修改。
1
2
3
4
5
6
7
|
class CMaster;
classCDog {
CMaster* m;
};
class CMaster {
CDog dogs[10];
};
|
这种就是人中有狗的复合关系,但是对于复合关系的定义:B是A的固有属性或组成部分是违和的,狗当人不是人的一部分。此外这种写法的缺陷在于,所有的狗都是在CMaster
里面定义的,对狗的一切操作都得在CMaster
里面进行,Dogs' lives matter!
,这种写法无疑是不合理的。
正确的写法是两者互相委托。对狗的操作直接在外部就可以实现。
1
2
3
4
5
6
7
|
class CMaster;
classCDog {
CMaster* pm;
};
class CMaster {
CDog *dogs[10];
};
|
多态
通过虚函数,我们可以实现在派生类和基类中调用同名函数(同参数表)动态地根据对象类型,调用该对象对应地参数。
虚函数
前面有virtual
关键词声明的类的成员函数就是虚函数,该关键词只需在声明的时候使用,定义的时候不需要。
对于派生类中同名同参函数不必须使用vitrual
进行声明,它也会是虚函数。
虚函数的调用:
-
通过基类指针调用派生类的虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class CBase {
public:
virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {
public :
virtual void SomeVirtualFunction() { }
};
int main() {
CDerived ODerived;
CBase * p = & ODerived;
p -> SomeVirtualFunction(); //调用哪个虚函数取决于p指向哪种类型的对象
return 0;
}
|
-
通过基类引用调用派生类的虚函数
1
2
3
4
5
6
7
|
//类定义同上
int main() {
CDerived ODerived;
CBase& r = ODerived;
r.SomeVirtualFunction(); //调用哪个虚函数取决于r引用哪种类型的对象
return 0;
}
|
纯虚函数和抽象类
纯虚函数样例:virtual T function() = 0;
包含纯虚函数的类叫抽象类:
- 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象
- 抽象类的指针和引用可以指向由抽象类派生出来的类的对象
抽象类的成员函数为什么可以调用纯虚函数呢?因为抽象类一定不能生成它的对象,即使在成员函数中调用该虚函数也一定是在派生类中实现的情况。
此外虚函数必须实现,如果不实现,编译器将报错。
一个派生类想要不是抽象类,则必须实现基类的所有虚函数。
意义与开销
多态是通过时间和空间成本来减少开发人员繁琐程度的。以下内容仅为胡扯。
在空间上:如果我们使用sizeof()
我们就会发现抽象类比正常类多8个字节
,需要存储虚函数表。
在时间上:每一次调用都需要判断,显而易见()
但是当我们想要对程序进行更新时,比如存在对派生类之间进行交互的函数,如果不采用多态的写法,我们就需要穷举每两个类之间的所有行为,无疑是冗杂又麻烦的,如果采用多态,只需要用基类作为参数,然后在不同派生类中实现相似的虚函数就可以。
析构函数
虚函数只存在虚析构函数,不存在虚构造函数。
其意义在于:通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数。但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。
解决办法:把基类的析构函数声明为virtual
- 派生类的析构函数可以virtual不进行声明
- 通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数
最后附上一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
#include <iostream>
using namespace std;
class A {
private:
int nVal;
public:
void Fun()
{ cout << "A::Fun" << endl; };
virtual void Do()
{ cout << "A::Do" << endl; }
};
class B:public A {
public:
virtual void Do()
{ cout << "B::Do" << endl;}
};
class C:public B {
public:
void Do( )
{ cout <<"C::Do"<<endl; }
void Fun()
{ cout << "C::Fun" << endl; }
};
void Call(A* p) {
p->Fun(); p->Do();
}
int main() {
Call( new A());
Call( new C());
return 0;
}
|
output:
A::Fun
A::Do
A::Fun
C::Do
输入输出暨文件处理
不得不说cin cout性能低下()
我们熟悉的iostream
是由istream
和osteam
这两个类派生而来。
istream是用于输入的流类, cin是该类的对象。
ostream是用于输出的流类, cout是该类的对象。
iostream是既能用于输入,又能用于输出的类。
ifstream是用于从文件读取数据的类。
ofstream是用于向文件写入数据的类。
fstream 是既能从文件读取数据,又能向文件写入数据的类。
标准流对象
1 .cin,表示标准输入的istream类对象。cin使我们可以从设备读如数据。
2. cout,表示标准输出的ostream类对象。cout使我们可以向设备输出或者写数据。
3 .cerr,表示标准错误的ostream类对象。cerr是导出程序错误消息的地方,它只能允许向屏幕设备写数据。
- clog,也是表示标准错误的ostream类对象。clog是标准日志流,也是只能向屏幕输出。
对于cerr和clog两者,cerr不使用缓存区,clog使用缓冲区。
在进行文件输出重定向后,进行调试或抛出异常的时候便应该使用cerr或clog,这样信息便不会写入文件中。
istream类的成员函数
-
istream & getline(char * buf, int bufSize);
istream & getline(char * buf, int bufSize,char delim);
delim
表示分隔符,getline
只会读入bufSize - 1
个字符,或者碰到delim
和\n
(有一个到的就截止),并在结尾自动添加'\0'
,\n
或delim
不会被读入。
getline
和cin
也可以通过while(cin >> x)
判断是否读入,具体原理下文系嗦。
-
bool eof();
判断是否结束
-
int peek();
返回下一个字符,但不从输入流中去掉
-
istream & putback(char c);
将字符c放入流
-
istream & ignore(int n = 1, int delim = EOF);
从流中最多删除n个字符,遇到delim
时结束。
重定向
事实上这是c的库。
FILE *freopen(const char *filename, const char *mode, FILE *stream)
模式 |
描述 |
“r” |
打开一个用于读取的文件。该文件必须存在。 |
“w” |
创建一个用于写入的空文件。如果文件名称与已存在的文件相同,则会删除已有文件的内容,文件被视为一个新的空文件。 |
“a” |
追加到一个文件。写操作向文件末尾追加数据。如果文件不存在,则创建文件。 |
“r+” |
打开一个用于更新的文件,可读取也可写入。该文件必须存在。 |
“w+” |
创建一个用于读写的空文件。 |
“a+” |
打开一个用于读取和追加的文件。 |
FILE *stream
可以为stdin
和stdout
。
流操纵算子
使用流操纵算子需要#include <iomanip>
- 整数流的基数:流操纵算子dec,oct,hex, setbase
1
2
3
4
5
6
7
8
9
|
int n = 10;
cout << n << endl;
cout << hex << n << '\n' << dec << n << '\n' << oct << n << endl;
/*
10
a
10
12
*/
|
- 浮点数的精度(precision, setprecision)
precision
是成员函数,其调用方式为:
cout.precision(5);
setprecision
是流操作算子,其调用方式为:
cout << setprecision(5); // 可以连续输出
保留位数和我们常见的一样分为有效位数和小数点后几位,如果想要用小数点后几位输出就需要setiosflags(ios::fixed)
,若取消则resetiosflags(ios::fixed)
。
如:cout << setiosflags(ios::fixed) << setprecision(6) << x << endl << resetiosflags(ios::fixed) << x ;
fixed
可以起到一个效果。
1
2
3
4
5
6
7
8
9
10
|
int main() {
double a = 114.12345678;
double b = 114514.191981;
cout << fixed << setprecision(6) << a << endl;
cout << b;
}
/*
114.123457
114514.191981
*/
|
可见保留位数也作用于下方输入。
setw(int n)
是流操作算子,width(int n)
是成员函数。
**宽度设置有效性是一次性的,在每次读入和输出之前都要设置宽度。 **
1
2
3
4
5
6
7
|
ostream &tab(ostream &output){
return output << '\t';
}
//调用:
cout << "aa" << tab << "bb";
//输出:
//aa bb
|
能这样自定义在于iostream类对«进行了重载。
ostream & operator <<( ostream & ( * p ) ( ostream & ) ) ;
该函数内部会调用p所指向的函数,且以 *this 作为参数 。(此处若不太理解,请看函数指针传参进行复习)
最后我们讨论以下为什么while(cin >> x)
是可以判断是否读入完毕的,while里面是布尔表达式,但是cin无疑是一个istream类型,(插入一句C89的布尔表达式类型竟然是int),所以istream类里面存在着隐式类型转换。此外在C++98和C++11上还存在不同。详细请看
文件
- 首先我们需要创建一个文件对象,类为
ifstream/ofstream/fstream
。
- 我们既可以采用初始化构造函数,也可以使用
open
成员函数,初始化对象。
fstream: 读/写模式打开文件, 如果文件不存在,以只读模式打开可以创建新文件,以读/写或写模式不能创建空文件
1
2
3
4
5
6
7
8
9
|
//读.1
ifstream file;
file.open("文件名", ios::openmode);
//读.2
ifstream file("文件名", ios::openmode);
//读写
fstream file("file.dat");
//ios::openmode即可以缺省,也可以多个连用。
ofstream file("f.dat", ios::out|ios::binary);
|
模式标志 |
描述 |
ios::app |
追加模式。所有写入都追加到文件末尾。 |
ios::ate |
文件打开后定位到文件末尾。 |
ios::in |
打开文件用于读取。 |
ios::out |
打开文件用于写入。 |
ios::trunc |
如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。 |
ios::binary |
以二进制方式打开。 |
以ios::binary
方式打开,换行符被解释成\r\n
,反之,换行符被解释成\n
。
- 输入输出
我们同样采用<<
和>>
进行操作,但是作用的对象不再是cin
和cout
,而是我们上文中声明的对象。但是<<
只能被ofstream
和fstream
类调用,同样的>>
被ifstream
和fstream
类调用。
- 文件位置指针
分为读指针和写指针。(特意声明下面函数参数通常是一个长整,在windows上long和int的大小是相同的,但却是是两种类型,1
和1L
是两种类型,但我相信它会有隐性类型转换的doge)
对于读指针:
1
2
3
4
5
6
|
ifstream fin("ex.dat");
long location = fin.tellg();//获得位置
fin.seekg(10L);//移动到第10个字节处,缺省的ios::beg
fin.seekg(location,ios::beg); //从头数location
fin.seekg(location,ios::cur); //从当前位置数location
fin.seekg(location,ios::end); //从尾部数location
|
对于写指针:
1
2
3
4
5
6
7
|
ofstream fout("a1.out");
long location = fout.tellp(); //取得写指针的位置
location = 10;
fout.seekp(location); // 将写指针移动到第10个字节处
fout.seekp(location,ios::beg); //从头数location
fout.seekp(location,ios::cur); //从当前位置数location
fout.seekp(location,ios::end); //从尾部数location
|
location可以为负数。我们可以注意到对于读和写只有g(get)和p(put)的区别。
- 显式关闭文件
对于从c过渡到c++的人来说,等作用于fclose(*fp)
的close()
不再是必须的,但是我们仍可以调用:
1
2
|
fstream f("ex.dat");
f.close();
|
- 二进制文件读写
ifstream 和 fstream的成员函数:
istream& read (char* s, long n);
将文件读指针指向的地方的n个字节内容,读入到内存地址s,然后将文件读指针向后移动n字节。
ofstream 和 fstream的成员函数:
istream& write (const char* s, long n);
将内存地址s处的n个字节内容,写入到文件中写指针指向的位置,然后将文件写指针向后移动n字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
#include <iostream>
#include <cstring>
#include <fstream>
using namespace std;
struct Student {
char name[20];
int score;
};
int main() {
Student s;
ofstream OutFile("students.dat",ios::out|ios::binary);
OutFile.close();
fstream File("students.dat",ios::in|ios::out|ios::binary);
while (cin >> s.name >> s.score){
File.write((char*)&s, sizeof(s));
}
File.seekp( 2 * sizeof(s),ios::beg); //定位写指针到第三个记录
char cbk[20] = "cbk";
File.write(cbk, sizeof(cbk));//写入cbk 20个字节
int x = 114;
File.write((char *)(&x), sizeof(int));
File.close();
File.open("students.dat");
while( File.read( (char* ) & s, sizeof(s) ) ) {
int readedBytes = File.gcount(); //看刚才读了多少字节
cout << "readedBytes = " << readedBytes << endl;
cout << s.name << " " << s.score << endl;
}
return 0;
}
|
出现了莫名其妙的bug,教训我们一定不要瞎寄吧缺省。(ios::binary虽然长还是写上为妙)
模板
对于不同的类型我们做的操作可能是相似的,却又不希望写多个函数进行重载,便可以通过模板来实现。
写在最前面,模板中的class
和typename
是等价的,class
也不是类的意思,只是标识符而已。
模板函数
1
|
template<class 参数1, class 参数2, ...> 返回值类型 函数(参数表){}
|
注意其实template
和函数是写在一起的,但是空格对最终表意不影响,所以我们通常将template
写在函数的上一行。(这样我们就无需担心作用域的问题)
我们现在就可以实现自己的swap
函数了。
1
2
3
4
5
6
|
template <class T>
void swap(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
}
|
对于多个参数
1
2
3
4
5
|
template <class T1, class T2>
T2 print(T1 arg1, T2 arg2) {
cout<< arg1 << " "<< arg2<<endl;
return arg2;
}
|
模板类
1
|
template <class type> class class-name {}
|
下面是自己实现的一个pair
类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
template <typename T1, typename T2>
class Pair {
public:
T1 first;
T2 second;
Pair() {}
Pair(T1 a, T2 b) : first(a), second(b) {}
bool operator < (const Pair<T1, T2> &p) const;
};
template <typename a, typename b>
bool Pair<a, b>::operator < (const Pair<a, b> &p) const {
return first < p.first;
}
int main() {
Pair<int, int> p[2] = {Pair(5, 2), Pair<int, int>(3, 4)};//两种写法都可以,如果编译器无法从上下文中推断出模板参数的类型,你就需要显式地提供模板参数。
sort(p, p + 2);
cout << p[0].first << " " << p[0].second << endl;
return 0;
}
|
类模板的<类型参数表>
中可以出现非类型参数 。
1
2
3
4
5
6
7
8
9
10
11
|
template <class T, int size>
class CArray {
T array[size];
public:
void Print() {
for (int i = 0;i < size; ++i)
cout << array[i] << endl;
}
};
CArray<double, 40> a2;
CArray<int, 50> a3;
|
派生
- 类模板从类模板派生
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//类模板A
template <class T1,class T2>
class A {
T1 v1; T2 v2;
};
//由类模板A派生的类模板B
template <class T1,class T2>
class B:public A<T2,T1> {//模板类A派生出模板类B。则,模板类A的参数也由模板类B的参数确定。
T1 v3; T2 v4;
};
//由类模板B派生的类模板C
template <class T>
class C:public B<T,T>{
T v5;
};
|
- 类模板从模板类派生
1
2
3
4
5
6
7
8
9
10
11
12
|
template <class T1,class T2>
class A {
T1 v1; T2 v2;
};
template <class T>
class B:public A<int,double> {
T v;
};
int main() {
B<char> obj1; //自动生成两个模板类: A<int,double> 和 B<char>
return 0;
}
|
- 模板从普通类派生
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//普通类
class A {
int v1;
};
//由普通类派生的模板诶
template <class T>
class B:public A {//所有从B实例化得到的类, 都以A为基类
T v;
};
int main() {
B<char> obj1;
return 0;
}
|
- 普通类从模板类派生
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
template <class T>
class A {
T v1;
int n;
};
class B:public A<int> {
double v;
};
int main() {
B obj1;
return 0;
}
|
静态成员
类模板中可以定义静态成员, 那么从该类模板实例化得到的所有类,都包含同样的静态成员 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
#include <iostream>
using namespace std;
#include <iostream>
using namespace std;
template <class T>
class A {
private:
static T count;
public:
A() { count++; }
~A() { count--; };
A(A&) { count++; }
static void PrintCount() { cout << count << endl; }
};
template<> int A<int>::count = 0;
template<> double A<double>::count = 0;
int main() {
A<int> ia;
A<double> da;
ia.PrintCount();
da.PrintCount();
return 0;
}
|
在C++中,模板特化允许你为模板定义一个特殊版本,这个版本只适用于特定的模板参数。模板特化的语法如下:
对于类模板特化:
1
2
3
4
|
template <>
class YourTemplateClass<YourSpecializedType> {
// 特化版本的定义
};
|
对于函数模板特化:
1
2
3
4
|
template <>
ReturnType YourTemplateFunction<YourSpecializedType>(YourSpecializedType arg) {
// 特化版本的定义
}
|
在上述代码中:
1
2
|
template<> int A<int>::count = 0;
template<> double A<double>::count = 0;
|
这是静态成员变量模板特化的例子。A<int>::count
和 A<double>::count
是模板类 A
的特化版本,对于 int
和 double
类型的实例,它们的 count
成员变量分别被初始化为 int
类型的 0
和 double
类型的 0
。