C++ 基础系列——类和对象
1. C++类的定义和对象的创建
1.1 类的定义
class Student{
public:
// 内联
void say(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}
private:
string name;
int age;
float score;
};
class 关键字定义类。Student 是类的名称,类名的首字母一般大写,以和其他的标识符区分开。
类只是一个模板(Template),编译后不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。只有在创建对象以后才会给成员变量分配内存。
1.2 在栈上创建对象
class Student
{
// ...
}
Student zhangSan; // 创建对象
1.3 在堆上创建对象
使用 new 关键字在堆上创建对象
Student *pStu = new Student;
在栈上创建出来的对象都有一个名字,在堆上分配内存,没有名字,只能得到一个指向它的指针,所以必须使用一个指针变量来接收这个指针,否则以后再也无法找到这个对象了,更没有办法使用它。
也就是说,使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。
栈内存是程序自动管理的,不能使用 delete 删除在栈上创建的对象;堆内存由程序员管理,对象使用完毕后可以通过 delete 删除。
1.4 访问类的成员
使用 . 号
Student stu;
stu.name = "小明";
stu.age = 15;
stu.score = 92.5f;
stu.say();
使用对象指针
Student *pStu = new Student;
pStu -> name = "小明";
pStu -> age = 15;
pStu -> score = 92.5f;
pStu -> say();
delete pStu; //删除对象
2. 类的成员变量和成员函数
class Student{
public:
//成员变量
char *name;
int age;
float score;
//成员函数
void say(); //函数声明
};
//函数定义
void Student::say(){
cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}
成员函数的定义一般放在类外,在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会。在类体内部定义的函数可以加 inline 关键字,但这是多余的,因为类体内部定义的函数默认就是内联函数。
3. 类成员的访问权限以及类的封装
-
C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,被称为成员访问限定符。所谓访问权限,就是你能不能使用该类中的成员。
-
C++ 中的 public、private、protected 只能修饰类的成员,不能修饰类,C++中的类没有公有私有之分。Java、c# 类有公私有之分。
-
类的默认权限是私有的,成员变量大都以 m_开头,struct 默认权限是 public。
-
private 关键字的作用在于更好地隐藏类的内部实现,该向外暴露的接口(能通过对象访问的成员)都声明为public,不希望外部知道、或者只在类内部使用的、或者对外部没有影响的成员,都建议声明为 private。
-
给成员变量赋值的函数通常称为 set 函数,它们的名字通常以 set 开头,后跟成员变量的名字;读取成员变量的值的函数通常称为 get 函数,它们的名字通常以 get 开头,后跟成员变量的名字。
-
所谓封装,是指尽量隐藏类的内部实现,只向用户提供有用的成员函数。
4. 对象的内存模型
-
类是创建对象的模板,不占用内存空间,不存在于编译后的可执行文件中;而对象是实实在在的数据,需要内存来存储。对象被创建时会在栈区或者堆区分配内存。
-
编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码。
-
对象的大小只受成员变量的影响,和成员函数没有关系。
5. 函数编译原理和成员函数的实现
- C++中的函数在编译时会根据它所在的命名空间、它所属的类、以及它的参数列表(也叫参数签名)等信息进行重新命名,形成一个新的函数名。这个新的函数名只有编译器知道,对用户是不可见的。对函数重命名的过程叫做名字编码(Name Mangling),是通过一种特殊的算法来实现的。
- 成员函数最终被编译成与对象无关的全局函数,因此为了能在成员函数中访问当前对象的成员变量,C++规定,编译成员函数时要额外添加一个参数,把当前对象的指针 this 传递进去,通过指针来访问成员变量。
- 对象调用成员函数时,不是通过对象找函数,而是通过函数找对象。
6. 构造函数
- 构造函数名字与类名相同,没有返回值,不需要用户显示调用,用户也不能调用,在创建用户时自动执行。
- 构造函数必须是 public 属性的,否则创建对象时无法调用。当然设置为 private、protected 属性也不会报错。
- 构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。
- 如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。
- 一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。
- 调用没有参数的构造函数也可以省略括号。
// 都会调用构造函数 Student()
Student stu();
Student stu;
Student *pstu = new Student();
Student *pstu = new Student;
7. 构造函数初始化列表
//采用初始化列表
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
//TODO:
}
- 使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简单明了。
- 成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。
- 初始化 const 成员变量的唯一方法就是使用初始化列表。
8. 析构函数
- 析构函数(Destructor) 也是一种特殊的成员函数,没有返回值,不需要显式调用(也没法显式调用),在销毁对象时会自动执行。
- 析构函数没有参数,不能被重载,因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。
class VLA{
public:
VLA(int len); //构造函数
~VLA(); //析构函数
void input(); //从控制台输入数组元素
void show(); //显示数组元素
private:
int *at(int i); //获取第 i 个元素的指针
const int m_len; //数组长度,必须使用初始化列表来初始化
int *m_arr; //数组指针
int *m_p; //指向数组第 i 个元素的指针
};
VLA::~VLA(){
delete[] m_arr; //释放内存
}
C++ 中的 new 和 delete 分别用来分配和释放内存,它们与 C 语言中 malloc()、free() 最大的一个不同之处在于:用 new 分配内存时会调用构造函数,用 delete 释放内存时会调用析构函数。
析构函数执行时机
析构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关。
- 在所有函数之外创建的对象是全局对象,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
- 在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
- new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。
class Demo{
public:
Demo(string s);
~Demo();
private:
string m_s;
};
Demo::Demo(string s): m_s(s){ cout << "constructor " << m_s << endl; }
Demo::~Demo(){ cout<<m_s<<endl; }
void func(){
//局部对象
Demo obj1("1");
}
//全局对象
Demo obj2("2");
int main(){
//局部对象
Demo obj3("3");
//new 创建的对象
Demo *pobj4 = new Demo("4");
func();
cout<<"main"<<endl;
return 0;
}
运行结果:
constructor 2
constructor 3
constructor 4
constructor 1
1
main
3
2
new 创建的对象没有 delete,不会调用析构函数。
9. C++对象数组
对象数组中的每个元素都需要用构造函数初始化。具体哪些元素用哪些构造函数初始化,取决于定义数组时的写法。
class CSample{
public:
CSample(){ //构造函数 1
cout<<"Constructor 1 Called"<<endl;
}
CSample(int n){ //构造函数 2
cout<<"Constructor 2 Called"<<endl;
}
};
int main(){
CSample arrayl[2]; // 无参构造函数
CSample array2[2] = {4, 5}; // 两个 int 参数的构造函数
CSample array3[2] = {3}; // 一个 int 参数、一个无参构造函数
CSample* array4 = new CSample[2]; // 两个无参构造函数
delete [] array4;
return 0;
}
在构造函数有多个参数时,数组的初始化列表中要显式地包含对构造函数的调用。
class CTest{
public:
CTest(int n){ } //构造函数(1)
CTest(int n, int m){ } //构造函数(2)
CTest(){ } //构造函数(3)
};
int main(){
//三个元素分别用构造函数(1)、(2)、(3) 初始化
CTest arrayl [3] = { 1, CTest(1,2) };
//三个元素分别用构造函数(2)、(2)、(1)初始化
CTest array2[3] = { CTest(2,3), CTest(1,2), 1};
//两个元素指向的对象分别用构造函数(1)、(2)初始化,pArray[3]没有初始化
CTest* pArray[3] = { new CTest(4), new CTest(1,2) };
return 0;
注意:指针数组只创建了两个对象,而不是三个。
10. 成员对象和封闭类
一个类的成员变量如果是另一个类的对象,就称之为“成员对象”。包含成员对象的类叫封闭类(enclosed class)。
成员对象初始化
成员对象的初始化需要借助封闭类构造函数的初始化列表。
//汽车类
class Car{
public:
Car(int price, int radius, int width);
void show() const;
private:
int m_price; // 价格
Tyre m_tyre; // 成员对象,轮胎类
Engine m_engine; // 成员对象,引擎类
};
Car::Car(int price, int radius, int width): m_price(price), m_tyre(radius, width) /*指明 m_tyre 对象的初始化方式*/
{
// ...
};
void Car::show() const {
cout << "价格:" << this->m_price << "¥" << endl;
this->m_tyre.show();
this->m_engine.show();
}
- m_tyre 应以 radius 和 width 作为参数调用 Tyre(int radius, int width) 构造函数初始化。
- m_engine 应该用 Engine 类的无参构造函数初始化。
成员对象的消亡
封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数。成员对象构造函数的执行次序和成员对象在类定义中声明的次序一致,与它们在构造函数初始化列表中出现的次序无关。
当封闭类对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数,成员对象析构函数的执行次序和构造函数的执行次序相反,即先构造的后析构,这是 C++ 处理此类次序问题的一般规律。
11. this 指针
this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。
class Student{
public:
void setname(char *name);
void setage(int age);
void setscore(float score);
void show();
private:
char *name;
int age;
float score;
};
void Student::setname(char *name){
this->name = name;
}
void Student::setage(int age){
this->age = age;
}
void Student::setscore(float score){
this->score = score;
}
void Student::show(){
cout<<this->name<<"的年龄是"<<this->age<<",成绩是"<<this->score<<endl;
}
this 是一个指针,要用 -> 来访问成员变量或成员函数。
this 虽然用在类的内部,但是只有在对象被创建以后才会给 this 赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给 this 赋值。
- this 是 const 指针,它的值是不能被修改的。
- this 只能在成员函数内部使用。
- 只有当对象被创建后 this 才有意义,不能在 static 成员函数中使用 this 指针。
this 到底是什么
- this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
- this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。
- 成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。这个额外的参数实际上就是 this,它是成员函数和成员变量关联的桥梁。
12. static 静态成员变量
静态成员变量是一种特殊的成员变量,它被关键字 static 修饰,被该类所有对象所共享。
static 成员变量必须在类声明的外部初始化,具体形式为:
type class::name = value;
int Student::m_total = 0;
静态成员变量在初始化时不能再加 static,但必须要有数据类型。被 private、protected、public 修饰的静态成员变量都可以用这种方式初始化。
- static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。
- static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。
- static 成员变量既可以通过对象来访问,也可以通过类来访问。
//通过类类访问 static 成员变量
Student::m_total = 10;
//通过对象来访问 static 成员变量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
//通过对象指针来访问 static 成员变量
Student *pstu = new Student("李华", 16, 96);
pstu -> m_total = 20;
一个完整的例子:
class Student{
public:
Student(char *name, int age, float score);
void show();
private:
static int m_total; //静态成员变量
char *m_name;
int m_age;
float m_score;
};
int Student::m_total = 0; //初始化静态成员变量
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
m_total++; //操作静态成员变量
}
void Student::show(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"(当前共有"<<m_total<<"名学生)"<<endl;
}
int main(){
//创建匿名对象
(new Student("小明", 15, 90)) -> show();
(new Student("李磊", 16, 80)) -> show();
(new Student("张华", 16, 99)) -> show();
(new Student("王康", 14, 60)) -> show();
return 0;
}
-
之所以使用匿名对象,是因为每次创建对象后只会使用它的 show() 函数,不再进行其他操作。不过使用匿名对象无法回收内存,会导致内存泄露,在中大型程序中不建议使用。
-
static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着 static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
-
静态成员变量必须初始化,而且只能在类体外进行。
-
静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。
13. static 静态成员函数
- 普通成员函数可以访问所有成员(包括成员变量和成员函数),静态成员函数只能访问静态成员。
- 静态成员函数没有 this 指针,不知道指向哪个对象,无法访问对象的成员变量,也就是说静态成员函数不能访问普通成员变量,只能访问静态成员变量。
- 静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
- 和静态成员变量类似,静态成员函数在声明时要加 static,在定义时不能加 static。静态成员函数可以通过类来调用(一般都是这样做),也可以通过对象来调用。
14. const 成员变量和成员函数
const 成员变量
const 成员变量的用法和普通 const 变量的用法相似,只需要在声明时加上 const 关键字。初始化 const 成员变量只有一种方法,就是通过构造函数的初始化列表。
const 成员函数
const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。
我们通常将 get 函数设置为常成员函数。读取成员变量的函数的名字通常以 get 开头,后跟成员变量的名字,所以通常将它们称为 get 函数。
class Student{
public:
Student(char *name, int age, float score);
void show();
//声明常成员函数
char *getname() const;
int getage() const;
float getscore() const;
private:
char *m_name;
int m_age;
float m_score;
};
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(){
cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
//定义常成员函数
char * Student::getname() const{
return m_name;
}
int Student::getage() const{
return m_age;
}
float Student::getscore() const{
return m_score;
}
需要强调的是,必须在成员函数的声明和定义处同时加上 const 关键字。
区分 const 的位置:
- 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型。
- 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值。
15. const 对象
一旦将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)。
定义常对象的语法和定义常量的语法类似:
const class object(params);
class const object(params);
当然也可以定义 const 指针:
const class *p = new class(params);
class const *p = new class(params);
16. 友元函数和友元类
借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员。
友元函数
在当前类以外定义的、不属于当前类的函数也可以在类中声明,但要在前面加 friend 关键字,这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。
友元函数可以访问当前类中的所有成员,包括 public、protected、private 属性的。
将非成员函数声明为友元函数
友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。
将其他类的成员函数声明为友元函数
class Address; //提前声明 Address 类
//声明 Student 类
class Student{
public:
Student(char *name, int age, float score);
void show(Address *addr);
private:
char *m_name;
int m_age;
float m_score;
};
//声明 Address 类
class Address{
private:
char *m_province; //省份
char *m_city; //城市
char *m_district; //区(市区)
public:
Address(char *province, char *city, char *district);
//将 Student 类中的成员函数 show()声明为友元函数
friend void Student::show(Address *addr);
};
//实现 Student 类
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(Address *addr){ // 定义为了 Address 类的友元函数,可以通过 Address 对象访问包括 private 权限的所有成员变量和函数
cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"区"<<endl;
}
//实现 Address 类
Address::Address(char *province, char *city, char *district){
m_province = province;
m_city = city;
m_district = district;
}
一般情况下,类必须在正式声明之后才能使用;但是一些情况下,只要做好提前声明也可以先使用。如上例中提前声明 Address 类。
友元类
如果将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的。
class Address; //提前声明 Address 类
//声明 Student 类
class Student{
public:
Student(char *name, int age, float score);
void show(Address *addr);
private:
char *m_name;
int m_age;
float m_score;
};
//声明 Address 类
class Address{
private:
char *m_province; //省份
char *m_city; //城市
char *m_district; //区(市区)
public:
Address(char *province, char *city, char *district);
//将 Student 类声明为 Address 类的友元类
friend class Student;
};
//实现 Student 类
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(Address *addr){ // Student 是 Address 的友元类,Student 类可以通过 Address 对象访问 Address 所有成员变量和函数。
cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"区"<<endl;
}
//实现 Address 类
Address::Address(char *province, char *city, char *district){
m_province = province;
m_city = city;
m_district = district;
}
friend class Student; // 将 Student 类声明为 Address 类的友元类
- 友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
- 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。
17. 类也是一种作用域
静态成员既可以通过对象访问,又可以通过类访问,而 typedef 定义的类型只能通过类来访问。
class A{
public:
typedef int INT;
static void show();
void work();
};
void A::show(){ cout<<"show()"<<endl; }
void A::work(){ cout<<"work()"<<endl; }
int main(){
A a;
a.work(); //通过对象访问普通成员
a.show(); //通过对象访问静态成员
A::show(); //通过类访问静态成员
A::INT n = 10; //通过类访问 typedef 定义的类型
return 0;
}
-
一个类就是一个作用域的事实能够很好的解释为什么我们在类的外部定义成员函数时必须同时提供类名和函数名。在类的外部,类内部成员的名字是不可见的。
-
一旦遇到类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体。结果就是,我们可以直接使用类的其他成员而无需再次授权了。
-
函数的返回值类型出现在函数名之前,当成员函数定义在类的外部时,返回值类型中使用的名字都位于类的作用域之外,此时必须指明该名字是哪个类的成员。
// 错误写法
PCHAR A::show(PCHAR str){
cout<<str<<endl;
n = 10;
return str;
}
// 返回值类型 PCHAR 出现在类名之前,所以事实上它是位于 A 类的作用域之外的。
A::PCHAR A::show(PCHAR str){
cout<<str<<endl;
n = 10;
return str;
}
18. class 和 struct 有什么区别
C 语言中,struct 只能包含成员变量,不能包含成员函数。
C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
C++ 中的 struct 和 class 基本是通用的,唯有几个细节不同:
- 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
- class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
- class 可以使用模板,而 struct 不能。
19. string 字符串详解
字符串的定义
// 使用 string 类需要包含头文件<string>,
#include <string>
int main(){
string s1; // 只定义但没有初始化,默认值是""空字符串
string s2 = "c plus plus"; // string 的结尾没有结束标志'\0'
string s3 = s2; //使用 s2 进行初始化
string s4 (5, 's'); // 5个s,也就是"sssss"
return 0;
}
转换为 C 风格的字符串
有时候必须要使用 C 风格的字符串(例如打开文件时的路径),为此 string 类为提供了一个转换函数 c_str(),该函数能够将 string 字符串转换为 C 风格的字符串,并返回该字符串的 const 指针(const char*)。
string path = "D:\\demo.txt";
FILE *fp = fopen(path.c_str(), "rt");
string 字符串的输入输出
string 类重载了输入输出运算符,可以像对待普通变量那样对待 string 变量,也就是用 >> 进行输入,用 << 进行输出。
string s;
cin >> s; //输入字符串
cout << s << endl; //输出字符串
输入运算符 >> 默认会忽略空格,遇到空格就认为输入结束。
访问字符串中的字符
string 字符串可以按照下标来访问其中的每一个字符。string 字符串的起始下标仍是从 0 开始。
string s = "1234567890";
for(int i=0,len=s.length(); i<len; i++){
cout<<s[i]<<" ";
}
字符串的拼接
有了 string 类,可以使用 + 或 += 运算符来直接拼接字符串。运算符的两边可以都是 string 字符串,还可以是一个 string 字符串和一个字符数组,或者是一个 string 字符串和一个单独的字符。
string s1 = "first ";
string s2 = "second ";
char *s3 = "third ";
char s4[] = "fourth ";
char ch = '@';
string s5 = s1 + s2;
string s6 = s1 + s3;
string s7 = s1 + s4;
string s8 = s1 + ch;
string 字符串的增删改查
插入字符串
insert() 函数可以在 string 字符串中指定的位置插入另一个字符串,它的一种原型为:
string& insert (size_t pos, const string& str);
更多 insert() 函数的原型和用法请参考:http://www.cplusplus.com/reference/string/string/insert/
删除字符串
erase() 函数可以删除 string 中的一个子字符串。它的一种原型为:
string& erase (size_t pos = 0, size_t len = npos);
提取子字符串
substr() 函数用于从 string 字符串中提取子字符串,它的原型为:
string substr (size_t pos = 0, size_t len = npos) const;
字符串查找
- find() 函数
find() 函数用于在 string 字符串中查找子字符串出现的位置,它其中的两种原型为:
size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;
-
rfind() 函数
rfind() 和 find() 很类似,同样是在字符串中查找子字符串,不同的是 find() 函数从第二个参数开始往后查找,而 rfind() 函数则最多查找到第二个参数处,如果到了第二个参数所指定的下标还没有找到子字符串,则返回一个无穷大值 4294967295。 -
find_first_of() 函数
find_first_of() 函数用于查找子字符串和字符串共同具有的字符在字符串中首次出现的位置。
string s1 = "first second second third";
string s2 = "asecond";
int index = s1.find_first_of(s2);
if(index < s1.length())
cout<<"Found at index : "<< index <<endl; // 3
else
cout<<"Not found"<<endl;
return 0;
20. string 的内部实现
在 C 语言中,有两种方式表示字符串,两种形式总是以\0 作为结束标志:
- 一种是用字符数组来容纳字符串,例如 char str[10] = "abc",这样的字符串是可读写的;
- 一种是使用字符串常量,例如 char *str = "abc",这样的字符串只能读,不能写。
C++ string 隐藏了它所包含的字符序列的物理表示。程序设计人员不必关心数组的维数或\0 方面的问题。
string 在内部封装了与内存和容量有关的信息。具体地说,C++ string 对象知道自己在内存中的开始位置、包含的字符序列以及字符序列长度;当内存空间不足时 string 还会自动调整,让内存空间增长到足以容纳下所有字符序列的大小。
C++ 标准没有定义在哪种确切的情况下应该为 string 对象分配内存空间来存储字符序列。string 内存分配规则明确规定:允许但不要求以引用计数(reference counting)的方式实现。但无论是否采用引用计数,其语义都必须一致。
string s1("12345");
string s2 = s1;
cout << (s1 == s2) << endl; // 1,true
s1[0] = '6';
cout << "s1 = " << s1 << endl; //62345
cout << "s2 = " << s2 << endl; //12345
cout << (s1 == s2) << endl; // 0,false
只有当字符串被修改的时候才创建各自的拷贝,这种实现方式称为写时复制(copy-on-write)策略。当字符串只是作为值参数(value parameter)或在其他只读情形下使用,这种方法能够节省时间和空间。
不论一个库的实现是不是采用引用计数,它对 string 类的使用者来说都应该是透明的。遗憾的是,情况并不总是这样。在多线程程序中,几乎不可能安全地使用引用计数来实现。
21. 总结
- 类只是一个模板,编译后不占用内存空间,所以定义类时不能对成员变量初始化,只有在创建变量后才会给成员变量分配内存。
- 栈上创建的对象有名字,堆上创建的对象没有名字,只有一个指向它的指针。栈内存程序自动管理,堆内存需要自己 delete 释放。
- 类中定义的成员函数会自动成为内联函数,可以不加 inline 关键字。
- C++ 类没有公私有之分。
- 类是创建对象的模板,不占用内存空间。编译器会将成员变量和成员函数分开存储,每个对象单独分配成员变量内存,但所有对象会共享同一函数代码。
- 对象大小只受成员变量影响。
- C++ 函数编译时会根据命名空间、类、参数列表等信息重新命名(名字编码)。成员函数最终会被编译成与对象无关的全局函数,为了联系对象,编译成员函数时会将 this 指针作为额外参数传递进去。
- 构造函数在创建对象时自动执行,且一定会执行。
- 调用没有参数的构造函数可以省略括号。
- 使用构造函数初始化列表无效率优势,仅书写方便,成员变量的初始化顺序只与成员变量在类中声明的顺序有关。
- 只能使用初始化列表初始化 const 成员变量。
- 析构函数没有参数,不能被重载,一个类只能有一个析构函数。析构函数执行顺序与构造函数相反。
- 构造函数有多个参数时,数组的初始化列表要显式包含构造函数的调用。
{ CTest(2,3), CTest(1,2), 1}
- 指针数组不会自动调用构造函数,需显式调用。
CTest* pArray[3] = { new CTest(4), new CTest(1,2) }; // 只创建两个对象
- 先执行成员对象的构造函数,再执行封闭类自己的构造函数。先构造后析构原则。
- this 指针在类的内部,只有在对象被创建后才会赋值。
- 不能在 static 成员函数中使用 this 指针。
- this 指针实际上是成员函数的一个形参,它是成员函数与成员变量关联的桥梁。
- static 成员变量必须在类外部初始化,其内存不是在声明类及创建对象时分配,而是类外全局初始化时分配。
- static 成员变量既可以通过对象来访问,也可以通过类来访问。
- 静态成员函数只能访问静态成员(无 this 指针),声明时加 static,定义时不需要加。
- const 成员变量只需要在声明时加上 const 关键字。
- const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,需要在声明和定义处同时加上 const 关键字,加在函数头部的结尾表示常成员函数。
- const 对象只能调用类的 const 成员(变量+函数)。
- 友元类和友元函数,表示其他类/函数可以通过当前类的对象访问所有权限成员,友元关系是单向的,且不能传递。
- 类也是一种作用域,参数列表和函数体共享类作用域,但返回值不共享,需要再次指明哪个类。
- C 语言中,struct 只能包含成员变量,不能包含成员函数。C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。
- C++ 中 struct 和 class 基本通用。几点区别:class 成员、继承关系都默认 private,struct 默认 public,class 可以使用模板,struct 不能。