类和对象
- 1. 类和对象的关系
- 2. 对象指针
- 3. 在堆上创建对象
- 4. 成员访问限定符
- 5. 名字编码(Name Mangling)
- 6.构造函数
- 7.构造函数的重载
- 8.初始化列表
- 8. 成员变量初始化的顺序(通过初始化列表)
- 9. 初始化 const 成员变量
- 10. 析构函数
- 11. 析构函数的执行时机!!
- 12. 对象数组及其初始化
- 12. 成员对象和封闭类
- 13. 成员对象的初始化和析构顺序
- 14. this 指针
- 15. static静态成员变量
- 16. static静态成员函数和普通成员函数的区别
- 17. const成员变量和const成员函数
- 18. const在函数的位置区别。
- 18. const对象(常对象)
- 19. 友元函数和友元类(friend关键字)
- 20. string类
- 21. 附录总结
引用:
[1]C语言中文网
1. 类和对象的关系
类是一种复杂数据类型的声明,并不占用内存空间。对象是类实例化后的具体实例,是类这个数据类型的一个变量,占用内存空间。
类的定义(声明)不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。只有在创建对象以后才会给成员变量分配内存,这个时候就可以赋值了。
类名的首字母一般大写
类定义的最后有一个分号";",它是类定义的一部分,表示类定义结束了,不能省略。
除了创建单个对象,还可以创建对象数组:
class Student {
int age;
char *name;
};
Student liyi; //创建单个对象
Student class_51[100]; //创建对象数组
liyi.age = 18; //创建对象以后才会给成员变量分配内存
liyi.name = 'liyi';
2. 对象指针
可以创建对象指针指向实例化后的对象。具体方法如下:
Student liyi; //创建单个对象
Student *pstu = &liyi;
3. 在堆上创建对象
知识点前提:栈区是程序自动管理的,不需要人为的释放内存。堆区是程序员自己管理的,必须自行释放内存。即,new和delete必须成对出现。
Student liyi; //在栈区实例化对象
Student *pstu = new Student; //在堆区实例化对象
我们在栈区创建的对象是有名字,即liyi。我们可以通过liyi这个对象名直接访问到对应的数据。所以,一般没必要用对象指针指向它。但是new操作符所创建的对象在堆区,返回的是一个指针,没有名字(简单来说就是,new创建的对象是匿名的)。如果没有对应的对象指针进行接收,则该对象无法访问了。
对象指针后,可以通过箭头->来访问对象的成员变量和成员函数。
通过对象名字访问成员使用点号.,通过对象指针访问成员使用箭头->。
4. 成员访问限定符
public: 类内可以访问,类外可以访问。
private:类内可以访问,类外不可以通过对象访问。
protected:类内可以访问,类外不可以通过对象访问,但是在它的派生类内部可以访问。
如果既不写 private 也不写 public,就默认为 private。
5. 名字编码(Name Mangling)
前提知识:C++对于对象进行分配内存时,只分配成员变量的内存。
不同对象的成员函数代码相同,所以不同对象分别分配自己的成员变量,同时共享成员函数代码。截图来自C语言中文网。
那么如何确定每个对象自己的成员函数代码呢。这就需要借助C++编译器的名字编码。C++在每次编译的时候,会根据所属类名,命名空间,参数列表等不同对成员函数名进行重新编码。
6.构造函数
- 构造函数会在创建对象时自动调用,不需要用户显式调用(用户也不能调用)。如果用户没有创建,则编译器会自动创建构造函数。一旦用户自己定义了构造函数,编译器则不会自动生成。
- 构造函数必须是public属性,其他属性虽然不会报错,但是创建的构造函数没有意义。
- 构造函数没有返回值,所以函数声明前不能有任何数据类型,并且函数体中不能有 return 语句。
- 构造函数在实际开发中往往用来做一些初始化工作。
7.构造函数的重载
构造函数是必须调用的,在创建对象的时候,必定会执行一个构造函数。如果构造函数重载,则必须有一组参数列表和其中一个构造函数相匹配。
8.初始化列表
class Student{
private:
char* m_name;
int m_age;
public:
// 初始化列表方法:
Student(char* name, int age):m_name(name), m_age(age){}
//等价于Student(char* name, int age){ m_name = name; m_age=age;}
}
使用构造函数初始化列表并没有效率上的优势,仅仅是书写方便。
8. 成员变量初始化的顺序(通过初始化列表)
成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。
class Student{
private:
int m_age;
int m_age2;
public:
Student(int age):m_age2(age), m_age(m_age2){}
//错误理解:Student(int age){ m_age2= age; m_age=m_age2;}
//正确理解:Student(int age){ m_age= m_age2; m_age2=age;}
}
顺序修改后:
所以,尽量按照成员变量的声明的顺序进行初始化。
9. 初始化 const 成员变量
初始化 const 成员变量的唯一方法就是使用初始化列表。
错误方法:
10. 析构函数
- 析构函数会在销毁对象时自动调用,不需要用户显式调用(用户也不能调用)。如果用户没有创建,则编译器会自动创建构造函数。析构函数是唯一的,不能重载!!!
- 析构函数必须是public属性,其他属性虽然不会报错,但是创建的析构函数没有意义。
- 析构函数没有返回值,所以函数声明前不能有任何数据类型,并且函数体中不能有 return 语句。
- 析构函数在实际开发中往往用来做一些销毁内存(new,malloc创建的堆区变量,关闭文件)。
示例:
11. 析构函数的执行时机!!
- 在函数外创建的对象,是全局对象,类似于全局变量。数据存储在全局数据区,当全局作用域结束后,程序自动销毁存储在全局数据区的对象,调用其析构函数。
- 在函数中创建的对象,是局部对象,类似于局部变量。数据存储在栈区,当函数结束后,程序自动销毁存储在栈区的对象,调用其析构函数。
- 通过new创建的对象,也是局部对象,但是其存储在堆区,由程序员自动管理。如果没有delete操作,则程序结束也不会调用其析构函数。所以,new申请的对象内存,一定要delete进行删除。
- 析构遵循,先构造的后析构原则。
举例如下:
12. 对象数组及其初始化
对象数组的初始化,如果只有一个参数,可以隐式调用。如果由多个参数则应该显式调用。
举例:
class Student{
private:
char* m_name;
int m_age;
public:
// 没有参数
Student():m_name("Null"),m_age(0){
cout<<" construct m_name is "<< m_name << ", m_age is "<<m_age<<endl;
}
// 只有一个参数
Student(char* name):m_name(name),m_age(0){
cout<<" construct m_name is "<< m_name << ", m_age is "<<m_age<<endl;
}
// 两个参数(可类比多个)
Student(char* name, int age):m_name(name), m_age(age){
cout<<" construct m_name is "<< m_name << ", m_age is "<<m_age<<endl;
}
~Student(){cout<<" destruct m_name is "<< m_name << ", m_age is "<<m_age<<endl;}
};
int main(){
// 如果不给参数,默认调用Student()
Student stu[2] = {};
// 如果只有一个参数,则可以隐式的直接给参数,例如"only_one_params"。
Student stu2[2] = {"only_one_params"};// 第一个是隐式调用,第二个是默认构造(也是隐式)
// 如果只有两个参数(多个),则需要显示调用,例如Student("only_two_params", 2)。
Student stu3[2] = {"only_one_params", Student("only_two_params", 2)}; // 第二个就是显示调用。
return 0;
}
注意:
如果是对象指针数组,则必须显示调用,并且创建几个对象就是几个对象。少的那个没有初始化,其指针值是随机的。
举例:
Student* stu4[3] = {
new Student("only_one_params"),
new Student("only_two_params", 2),
};
同时也可以验证之前的观点,new创建的对象,如果没有delete则不会进行析构。
12. 成员对象和封闭类
- 当类中的成员变量是对象时,被称为成员对象。
- 包含成员对象的类叫封闭类。
13. 成员对象的初始化和析构顺序
当封闭类创建对象时,需要先创建成员对象。对于成员对象的初始化,需要借助封闭类的构造函数初始化列表方法。
特殊情况,当封闭类的构造函数没有对成员对象进行初始化时,如果成员对象有无参数的构造函数。则会通过无参数的构造函数进行初始化,如果有参数,则会报错。因此,封闭类必须对成员对象进行初始化。
成员对象的构造和析构,遵循先构造后析构的原则。即,先对成员对象进行构造,再对封闭类对象进行构造。先对封闭类对象进行析构,再对成员对象进行析构。
举例:
class Age{
private:
int m_age;
public:
//无参数的构造函数
Age():m_age(0){cout<<" construct Age , m_age is "<<m_age<<endl;}
Age(int age):m_age(age){cout<<" construct Age, m_age is "<<m_age<<endl;}
~Age(){cout<<" destruct Age, m_age is "<<m_age<<endl;}
};
class Name{
private:
char* m_family_name;
char* m_last_name;
public:
Name(char* f_name, char* l_name):m_family_name(f_name), m_last_name(l_name){cout<<" construct is "<< m_family_name<<" " << m_last_name <<endl;}
~Name(){cout<<" destruct is "<< m_family_name<<" " << m_last_name <<endl;}
};
// 封闭类
class Student{
private:
// 成员对象
Name m_c_name;
Age m_c_age;
public:
// 初始化构建Age类和Name类的成员对象。
Student(char* f_name, char* l_name, int age):m_c_name(f_name, l_name), m_c_age(age){cout<<" construct Student is Class"<<endl;};
// 测试是否调用Age类中无参数构造函数的成员对象
Student(char* f_name, char* l_name):m_c_name(f_name, l_name){cout<<" construct Student is Class"<<endl;};
~Student(){cout<<" destruct Student is Class"<<endl;}
};
int main(){
Student stu("li", "si", 22);
// Student stu2("zhang", "san");
return 0;
}
从结果看,可以发现先通过构造函数创建成员对象m_c_name和成员对象m_c_age(顺序是成员对象声明顺序和初始化列表顺序无关),然后再通过封闭类构造函数创建对象stu。析构时的顺序先是封闭类析构函数,再是成员类析构函数。遵循先构造后析构原则。
(成员类析构函数顺序也是先构造后析构)
Student stu2("zhang", "san"); //会调用无参数的构造函数
14. this 指针
this指针是一个const 指针,指向当前对象,且作用域范围是在成员函数内部。调用方法为"->"。
注意:
- this 是 const 指针,它的值是不能被修改的。
- this 只能在成员函数内部使用。
- 只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用。
- this 实际上是成员函数的一个隐式形参。
15. static静态成员变量
不同对象之间成员变量相互独立。但是有时想要多个对象共享一个成员变量,此时就可以通过静态成员变量来实现。静态成员变量是一种特殊的成员变量,它被关键字static修饰。
静态成员变量在类内声明,属于类,不属于对象。
static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。
初始化:
- static 成员变量必须在类声明的外部初始化
- 静态成员变量在初始化时不能再加 static,但必须要有数据类型。
- static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。
static 成员变量和普通的 static 变量类似,都在内存分区中的全局数据区分配内存,到程序结束时才释放。
初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。[1]
静态成员变量既可以通过对象名访问,也可以通过类名访问。
举例:
class Student{
private:
char* m_name;
int m_age;
public:
static int m_num;
public:
// 初始化构建Age类和Name类的成员对象。
Student(char* name, int age):m_name(name), m_age(age){
m_num ++;
};
~Student(){}
void show();
};
void Student::show(){
cout << "class num is "<<m_num<<endl;
}
// 在类外对静态成员变量进行初始化。该数据存储在全局数据区,程序结束自动销毁。
int Student::m_num = 0;
int main(){
// 通过对象调用。
(new Student("zhangsan", 20))->show();
(new Student("lisi", 18))->show();
(new Student("wangwu", 22))->show();
//直接通过类名直接调用。
cout << "check class num by class name. the num is "<<Student::m_num<<endl;
return 0;
}
/*
输出:
class num is 1
class num is 2
class num is 3
check class num by class name. the num is 3
*/
16. static静态成员函数和普通成员函数的区别
静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
类似于python中的@staticmethod修饰的成员函数,其中没有self变量。
17. const成员变量和const成员函数
const成员变量只能通过初始化列表进行初始化,这点在第9点说过。
const成员函数可以访问类中所有的成员变量,但是不能修改它们的值。目的是为了保护数据。
方法是在成员函数头的尾部加上const关键字。
举例:
class Student{
private:
char* m_name;
int m_age;
public:
// 声明时函数头后加const关键字
int getAge() const;
public:
Student(char* name, int age):m_name(name), m_age(age){};
~Student(){}
};
// 定义时,函数头后也加const关键字
int Student::getAge() const{
return m_age;
}
int main(){
Student stu("zhaoliu", 25);
int num = stu.getAge();
cout << "check class num by class name. the num is "<<num<<endl;
return 0;
}
const成员函数不能修改内部的成员变量。如下图:
18. const在函数的位置区别。
- 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如const char * getname()。
- 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const。
18. const对象(常对象)
如果对象被const修饰,则被称为常对象。在常对象中,只能使用const成员变量和const成员函数。
19. 友元函数和友元类(friend关键字)
当外部函数想要访问一个类中的private修饰的成员变量和成员函数时,可以借助friend关键字进行修饰,如果修饰的函数,则是友元函数,如果修饰的是类,则是友元类(友元类中所有的成员函数都是友元函数。)
- 友元函数可以类外的函数(全局函数),也可以是类内的成员函数。
- friend关键字一定在类内。其声明的函数或者类,称为友元函数和友元类,可以访问该类中的所有成员变量。
- friend修饰的函数,一定要有对象,对象指针或者对象引用。类比于this指针,友元函数和友元类中的成员函数,都需要借助对象对该类的private成员进行访问。
- 友元函数和友元类是单向的不是双向的,例如B是A的友元类,可以访问A中所有成员变量。但是A不是B的友元类。
- 友元函数和友元类不可传递,例如B是A的友元类,A是C的友元类,但是B不是C的友元类。
全局函数:
class Student{
private:
char* m_name;
int m_age;
public:
Student(char* name, int age):m_name(name), m_age(age){};
~Student(){}
// 第二步:声明为Student类的友元函数,我们要访问Student类,就把friend定义到Student类中。
friend void show(Student * ptu);
};
//类外的全局函数想要访问Student类中的私有成员变量。
//第一步:参数需要有对象、对象指针或者对象引用
void show(Student * ptu){
cout<< "this student name is "<< ptu->m_name << " , age is "<<ptu->m_age<<endl;
}
int main(){
Student stu("zhaoliu", 25);
// 直接调用全局函数show(),并把对象传进去。
show(&stu);
return 0;
}
/*
输出:
this student name is zhaoliu , age is 25
*/
类内的成员函数:
class Student; // 提前声明,不然void show(Student * ptu);中Student未定义。
class Test{
//类内的成员函数想要访问Student类中的私有成员变量。
//第一步:参数需要有对象、对象指针或者对象引用
public:
void show(Student * ptu);
};
class Student{
private:
char* m_name;
int m_age;
public:
Student(char* name, int age):m_name(name), m_age(age){};
~Student(){}
// 第二步:声明为Student类的友元函数,我们要访问Student类,就把friend定义到Student类中。
friend void Test::show(Student * ptu);
};
void Test::show(Student * ptu){
cout<< "this student name is "<< ptu->m_name << " , age is "<<ptu->m_age<<endl;
}
int main(){
Student stu("zhaoliu", 25);
// 直接调用全局函数show(),并把对象传进去。
Test test;
test.show(&stu);
return 0;
}
/*
输出:
this student name is zhaoliu , age is 25
*/
友元类:
// 除了friend void Test::show(Student * ptu);改为 friend Test;
// 其他把和类内成员函数声明为友元函数一样
class Student{
private:
char* m_name;
int m_age;
public:
Student(char* name, int age):m_name(name), m_age(age){};
~Student(){}
// 第二步:声明为Student类的友元类,我们要访问Student类,就把friend定义到Student类中。
friend Test;
};
void Test::show(Student * ptu){
cout<< "this student name is "<< ptu->m_name << " , age is "<<ptu->m_age<<endl;
}
除非有必要,一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些。
20. string类
- 相比C语言中的字符串,string类对象声明的字符串数据,末尾不包括’\0’。
- string类可以直接用 “=”进行赋值。
- string类中有length()函数,该函数直接返回字符串的长度,因为末尾没有’\0’,所以返回的也是真是字符串长度。
- string 类可以使用+或+=运算符来直接拼接字符串,不需要使用C语言中的 strcat()、strcpy()、malloc() 等函数来拼接字符串了,也不用担心空间不够会溢出。
- string类转化为c语言的字符串,可以通过 “.c_str()”来实现。
string类的增上改查:
- 插入字符串:
int main(){
string s1, s2;
s1 = "1234567890";
s2 = "aaa";
s1.insert(2, s2);//第一个是字符串下表,第二个是插入的字符或字符串
cout<< s1 <<endl;
return 0;
}
/*
输出:12aaa34567890
*/
- 删除字符串:
int main(){
string s1;
s1 = "1234567890";
s1.erase(5);// 第二个参数如果不指定,则从第一个下标(包含)后到末尾全删除
cout<< s1 <<endl;
s1.erase(2, 1); // 第一个是字符串下标,第二个是要删除几个字符。
cout<< s1 <<endl;
return 0;
}
/*
输出:
12345
1245
*/
- 提取子字符串
int main(){
string s1, s2;
s1 = "1234567890";
s2= s1.substr(5, 2);// 第一个是字符串下标,第二个是从下标开始(包含)提取多少个字符
cout<< s2 <<endl;
return 0;
}
/*
输出:
67
*/
- 字符串查找
int main(){
string s1, s2;
s1 = "1234567890";
s2 = "567";
int index= s1.find(s2, 2); // 第一个参数是需要查找的子字符串,第二个参数是从第几个字符开始查,如果不指定则从第0个字符开始查。
cout<< index <<endl;
return 0;
}
/*
输出:
4
*/
还有其他两个查找函数:
rfind()函数:在find函数中,第二个参数表示从该参数下标开始查,而rfind函数则是从0字符查到第二个参数的下标。如果查不到返回-1
int main(){
string s1, s2;
s1 = "1234567890";
s2 = "567";
int index= s1.find(s2, 2);
cout<< index <<endl;
int index2= s1.rfind(s2, 2);
cout<< index2 <<endl;
int index3= s1.rfind(s2, 6);
cout<< index3 <<endl;
return 0;
}
/*
输出:
4
-1
4
*/
find_first_of() 函数:find_first_of() 函数用于查找子字符串和字符串共同具有的字符在字符串中首次出现的位置。
int main(){
string s1, s2;
s1 = "123 234 345";
s2 = "a32";
int index= s1.find_first_of(s2);
cout<< index <<endl;
return 0;
}
/*
输出:
1
*/
s1和s2共有的字符有“3”和“2”,在这两个中,首次出现下标是1。
21. 附录总结
该总结引用自[1]C语言中文网,如果想学习更详细的内容,可以去该网址学习。
-
类的成员有成员变量和成员函数两种。
-
成员函数之间可以互相调用,成员函数内部可以访问成员变量。
-
私有成员只能在类的成员函数内部访问。默认情况下,class 类的成员是私有的,struct 类的成员是公有的。
-
可以用“对象名.成员名”、“引用名.成员名”、“对象指针->成员名”的方法访问对象的成员变量或调用成员函数。成员函数被调用时,可以用上述三种方法指定函数是作用在哪个对象上的。
-
对象所占用的存储空间的大小等于各成员变量所占用的存储空间的大小之和(如果不考虑成员变量对齐问题的话)。
-
定义类时,如果一个构造函数都不写,则编译器自动生成默认(无参)构造函数和复制构造函数。如果编写了构造函数,则编译器不自动生成默认构造函数。一个类不一定会有默认构造函数,但一定会有复制构造函数。
-
任何生成对象的语句都要说明对象是用哪个构造函数初始化的。即便定义对象数组,也要对数组中的每个元素如何初始化进行说明。如果不说明,则编译器认为对象是用默认构造函数或参数全部可以省略的构造函数初始化。在这种情况下,如果类没有默认构造函数或参数全部可以省略的构造函数,则编译出错。
-
对象在消亡时会调用析构函数。
-
每个对象有各自的一份普通成员变量,但是静态成员变量只有一份,被所有对象所共享。静态成员函数不具体作用于某个对象。即便对象不存在,也可以访问类的静态成员。静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数。
-
常量对象上面不能执行非常量成员函数,只能执行常量成员函数。
-
包含成员对象的类叫封闭类。任何能够生成封闭类对象的语句,都要说明对象中包含的成员对象是如何初始化的。如果不说明,则编译器认为成员对象是用默认构造函数或参数全部可以省略的构造函数初始化。
-
在封闭类的构造函数的初始化列表中可以说明成员对象如何初始化。封闭类对象生成时,先执行成员对象的构造函数,再执行自身的构造函数;封闭类对象消亡时,先执行自身的析构函数,再执行成员对象的析构函数。
-
const 成员和引用成员必须在构造函数的初始化列表中初始化,此后值不可修改。
-
友元分为友元函数和友元类。友元关系不能传递。
-
成员函数中出现的 this 指针,就是指向成员函数所作用的对象的指针。因此,静态成员函数内部不能出现 this 指针。成员函数实际上的参数个数比表面上看到的多一个,多出来的参数就是 this 指针。