C++基本概念理解
1.Hello World的编译过程
#include<iostream> using namespace std; int main() { cout << "Hello world\n" << endl; system("pause"); return 0; }
2.C++头文件
- iostream.h:用于控制台输入输出头文件
- fstream.h:用于文件操作的头文件
- complex.h:用于复数计算的头文件
类、函数、宏等都统一纳入一个命名空间,这个命名空间的名字就是std
,所以使用最基本操作时需要添加using namespace std,实际开发中可以直接简写成 【std::调用类型】,老式C++一般具有.h,而新式C++为了兼容老式原有代码,进行了声明写法的简化
总结:
1) 旧的 C++ 头文件,如 iostream.h、fstream.h ,不在命名空间 std 中
2) 新的 C++ 头文件,如 iostream、fstream,在命名空间 std 中
3) 标准C头文件如 stdio.h、stdlib.h 等继续被支持。头文件的内容不在 std 中
4) 具有C库功能的新C++头文件具有如 cstdio、cstdlib 这样的名字。它们提供的内容和相应的旧的C头文件相同,内容在 std 中
3.动态分配和释放内存
new 和 delete:new 用来动态分配内存,delete 用来释放内存
int *p = new int[10]; //分配10个int型的内存空间 delete[] p;
4.指针引用和内联函数理解
#include<iostream> using namespace std; inline void swap(int* a,int* b) { int temp; temp = *a; *a = *b; *b = temp; } int main() { int m, n; cin >> m >> n; swap(&m, &n); cout << "最终结果是" << m << n; }
分别开辟a和b的内存地址,将其地址存入temp变量,进行交换,
main调用内联函数swap方法,传入m,n变量的地址,通过调用函数,完成地址交换
*开辟的内存地址是随机的,变量生成的地址是不变的
*是对内存的取地址的开辟操作,&是对变量的取地址操作。把变量的地址放入到开辟的内存里,然后完成交换
简写:编译器可能会将 *(&m)、*(&n) 分别优化为 m、n
int temp; temp = *(&m); *(&m) = *(&n); *(&n) = temp;
取变量m的地址,在找到其在内存中的地址,放入temp,
此时m中的内存空间为空状态,再取到变量n的地址,找到其在内存中的地址,赋给m,
最后将temp里的地址放到n里
为了提高效率,引入了内联函数,但是一般只将那些短小的、频繁调用的函数声明为内联函数
5.默认参数的理解:
为了方便函数调用时,不用在输入参数,直接执行默认参数的结果:
默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值
使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量
感觉默认函数就是另外一种多态
对:
void func(int a, int b=10, int c=20){ } void func(int a, int b, int c=20){ }
错:
void func(int a, int b=10, int c=20, int d){ } void func(int a, int b=10, int c, int d=20){ }
6.函数重载理解:
就是为了传参和写法统一、方便
- 函数名称必须相同
- 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)
- 函数的返回类型可以相同也可以不相同
- 仅仅返回类型不同不足以成为函数的重载
类定义和对象的创建
类只是一个模板,编译后不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。只有在创建对象以后才会给成员变量分配内存,这个时候就可以赋值了
对象指针
创建的对象 stu 在栈上分配内存,需要使用&
获取它的地址,pStu 是一个指针,它指向 Student 类型的数据,也就是通过 Student 创建出来的对象。例如:
Student stu;
Student *pStu = &stu;
在堆上创建对象,这个时候就需要使用前面讲到的new
关键字,例如:
Student *pStu = new Student;
使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数
栈内存是程序自动管理的,不能使用 delete 删除在栈上创建的对象;
堆内存由程序员管理,对象使用完毕后可以通过 delete 删除。在实际开发中,new 和 delete 往往成对出现,以保证及时删除不再使用的对象,防止无用内存堆积
#include <iostream> using namespace std; class Student{ public: char *name; int age; float score; void say(){ cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl; } }; int main(){ Student *pStu = new Student; pStu -> name = "小明"; pStu -> age = 15; pStu -> score = 92.5f; pStu -> say(); delete pStu; //删除对象 return 0; }
7.类的成员变量和成员函数
class Student{ public: //成员变量 char *name; int age; float score; //成员函数 void say(); //函数声明 }; //函数定义 void Student::say(){ cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl; }
成员函数必须先在类体中作原型声明,然后在类外定义,也就是说类体的位置应在函数定义之前
在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会
8.类成员的访问权限以及类的封装
C++ 中的 public、private、protected 只能修饰类的成员,不能修饰类,C++中的类没有共有私有之分
Student 类来演示成员的访问权限
#include <iostream> using namespace std; //类的声明 class Student{ private: //私有的 char *m_name; int m_age; float m_score; public: //共有的 void setname(char *name); void setage(int age); void setscore(float score); void show(); }; //成员函数的定义 void Student::setname(char *name){ m_name = name; } void Student::setage(int age){ m_age = age; } void Student::setscore(float score){ m_score = score; } void Student::show(){ cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl; } int main(){ //在栈上创建对象 Student stu; stu.setname("小明"); stu.setage(15); stu.setscore(92.5f); stu.show(); //在堆上创建对象 Student *pstu = new Student; pstu -> setname("李华"); pstu -> setage(16); pstu -> setscore(96); pstu -> show(); return 0; }
在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员
总结:
成员函数 setname()、setage() 和 setscore() 被设置为 public 属性,是公有的,可以通过对象访问。从而只能通过public属性的成员函数改变private里面的属性值
错误:无法直接访问类中的private
Student stu; //m_name、m_age、m_score 是私有成员变量,不能在类外部通过对象访问 stu.m_name = "小明"; stu.m_age = 15; stu.m_score = 92.5f; stu.show();
成员变量以及只在类内部使用的成员函数(只被成员函数调用的成员函数)都建议声明为 private,而只将允许通过对象调用的成员函数声明为 public
protected 的成员在类外也不能通过对象访问,但是在它的派生类内部可以访问
给成员变量赋值的函数通常称为 set 函数,它们的名字通常以set
开头,后跟成员变量的名字;读取成员变量的值的函数通常称为 get 函数,它们的名字通常以get
开头,后跟成员变量的名字
除了 set 函数和 get 函数,在创建对象时还可以调用构造函数来初始化各个成员变量
既不写 private 也不写 public,就默认为 private
9.构造函数
名字和类名相同,没有返回值,不需要用户显式调用,在创建对象时自动执行。这种特殊的成员函数就是构造函数
#include <iostream> using namespace std; class Student{ private: char *m_name; int m_age; float m_score; public: //声明构造函数 Student(char *name, int age, float score); //声明普通成员函数 void show(); }; //定义构造函数 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; } int main(){ //创建对象时向构造函数传参 Student stu("小明", 15, 92.5f); stu.show(); //创建对象时向构造函数传参 Student *pstu = new Student("李华", 16, 96); pstu -> show(); return 0; }
在栈上创建对象时,实参位于对象名后面,例如Student stu("小明", 15, 92.5f)
;
在堆上创建对象时,实参位于类名后面,例如new Student("李华", 16, 96)
构造函数必须是 public 属性的,否则创建对象时无法调用
- 不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void 也不允许;
- 函数体中不能有 return 语句。
构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的
如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用
一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成
调用没有参数的构造函数也可以省略括号。在栈上创建对象可以写作Student stu()
或Student stu
,在堆上创建对象可以写作Student *pstu = new Student()
或Student *pstu = new Student
,它们都会调用构造函数 Student()
构造函数的一项重要功能是对成员变量进行初始化
//采用初始化列表 Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ //TODO: }
这个语句的意思相当于函数体内部的m_name = name; m_age = age; m_score = score;
语句,也是赋值的意思
成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关
#include <iostream> using namespace std; class Demo{ private: int m_a; int m_b; public: Demo(int b); }; Demo::Demo(int b): m_b(b), m_a(m_b){ m_a = m_b; m_b = b; }
成员变量的赋值顺序由它们在类中的声明顺序决定,在 Demo 类中,先声明的 m_a,再声明的 m_b
构造函数初始化列表还有一个很重要的作用,那就是初始化 const 成员变量。初始化 const 成员变量的唯一方法就是使用初始化列表
class VLA{ private: const int m_len; int *m_arr; public: VLA(int len); }; //必须使用初始化列表来初始化 m_len VLA::VLA(int len): m_len(len){ m_arr = new int[len]; }
10.析构函数
创建对象:调用构造函数
销毁对象:调用析构函数,在销毁对象时自动执行
构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~
注意:析构函数没有参数,不能被重载,因此一个类只能有一个析构函数
#include <iostream> using namespace std; class VLA{ public: VLA(int len); //构造函数 ~VLA(); //析构函数 public: void input(); //从控制台输入数组元素 void show(); //显示数组元素 private: int *at(int i); //获取第i个元素的指针 private: const int m_len; //数组长度 int *m_arr; //数组指针 int *m_p; //指向数组第i个元素的指针 }; VLA::VLA(int len): m_len(len){ //使用初始化列表来给 m_len 赋值 if(len > 0){ m_arr = new int[len]; /*分配内存*/ } else{ m_arr = NULL; } } VLA::~VLA(){ delete[] m_arr; //释放内存 } void VLA::input(){ for(int i=0; m_p=at(i); i++){ cin>>*at(i); } } void VLA::show(){ for(int i=0; m_p=at(i); i++){ if(i == m_len - 1){ cout<<*at(i)<<endl; } else{ cout<<*at(i)<<", "; } } } int * VLA::at(int i){ if(!m_arr || i<0 || i>=m_len){ return NULL; } else{ return m_arr + i; } } int main(){ //创建一个有n个元素的数组(对象) int n; cout<<"Input array length: "; cin>>n; VLA *parr = new VLA(n); //输入数组元素 cout<<"Input "<<n<<" numbers: "; parr -> input(); //输出数组元素 cout<<"Elements: "; parr -> show(); //删除数组(对象) delete parr; return 0; }
~VLA()
就是 VLA 类的析构函数,它的唯一作用就是在删除对象(delete parr;)后释放已经分配的内存
用 new 分配内存时会调用构造函数,用 delete 释放内存时会调用析构函数
析构函数的执行时机
#include <iostream> #include <string> using namespace std; class Demo{ public: Demo(string s); ~Demo(); private: string m_s; }; Demo::Demo(string s): m_s(s){ } 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; }
全局对象:程序在结束执行时会调用这些对象的析构函数
局部对象:函数执行结束时会调用这些对象的析构函数
new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行
11.static静态成员变量
#include <iostream> using namespace std; class Student{ public: Student(char *name, int age, float score); void show(); private: static int m_total; //静态成员变量 private: 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; }
1) 一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它
2) static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存
3) 静态成员变量必须初始化,而且只能在类体外进行。例如:int Student::m_total = 10;
初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值
4) 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存
12.static静态成员函数
静态成员函数只能访问静态成员,不管有没有创建对象,都可以调用静态成员函数
#include <iostream> using namespace std; class Student{ public: Student(char *name, int age, float score); void show(); public: //声明静态成员函数 static int getTotal(); static float getPoints(); private: static int m_total; //总人数 static float m_points; //总成绩 private: char *m_name; int m_age; float m_score; }; int Student::m_total = 0; float Student::m_points = 0.0; Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ m_total++; m_points += score; } void Student::show(){ cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl; } //定义静态成员函数 int Student::getTotal(){ return m_total; } float Student::getPoints(){ return m_points; } int main(){ (new Student("小明", 15, 90.6)) -> show(); (new Student("李磊", 16, 80.5)) -> show(); (new Student("张华", 16, 99.0)) -> show(); (new Student("王康", 14, 60.8)) -> show(); int total = Student::getTotal(); float points = Student::getPoints(); cout<<"当前共有"<<total<<"名学生,总成绩是"<<points<<",平均分是"<<points/total<<endl; return 0; }
静态成员函数的主要目的是访问静态成员。getTotal()、getPoints() 也可以声明为普通成员函数,但是它们都只对静态成员进行操作,加上 static 语义更加明确
和静态成员变量类似,静态成员函数在声明时要加 static,在定义时不能加 static
13.const成员函数(常成员函数)
const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值
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 char * getname()
- 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如
char * getname() const
14.const对象(常对象)
将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)
#include <iostream> using namespace std; class Student{ public: Student(char *name, int age, float score); public: 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; } int main(){ const Student stu("小明", 15, 90.6); //stu.show(); //error cout<<stu.getname()<<"的年龄是"<<stu.getage()<<",成绩是"<<stu.getscore()<<endl; const Student *pstu = new Student("李磊", 16, 80.5); //pstu -> show(); //error cout<<pstu->getname()<<"的年龄是"<<pstu->getage()<<",成绩是"<<pstu->getscore()<<endl; return 0; }
stu、pstu 分别是常对象以及常对象指针,它们都只能调用 const 成员函数
15.C++友元函数和友元类(C++ friend关键字)
友元函数(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的 private 成员
友元函数可以访问当前类中的所有成员,包括 public、protected、private 属性的
friend 函数不仅可以是全局函数(非成员函数),还可以是另外一个类的成员函数
一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员
#include <iostream> using namespace std; class Address; //提前声明Address类 //声明Student类 class Student{ public: Student(char *name, int age, float score); public: 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){ 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; } int main(){ Student stu("小明", 16, 95.5f); Address addr("陕西", "西安", "雁塔"); stu.show(&addr); Student *pstu = new Student("李磊", 16, 80.5); Address *paddr = new Address("河北", "衡水", "桃城"); pstu -> show(paddr); return 0; }
上例将 Student 类的成员函数 show() 声明为 Address 类的友元函数,由此,show() 就可以访问 Address 类的 private 成员变量了
总结:
友元函数方便跨类使用变量或函数
友元类
不仅可以将一个函数声明为一个类的“朋友”,还可以将整个类声明为另一个类的“朋友”,这就是友元类。友元类中的所有成员函数都是另外一个类的友元函数
例如将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的
//声明Student类 class Student{ public: Student(char *name, int age, float score); public: void show(Address *addr); private: char *m_name; int m_age; float m_score; }; //声明Address类 class Address{ public: Address(char *province, char *city, char *district); public: //将Student类声明为Address类的友元类 friend class Student; private: char *m_province; //省份 char *m_city; //城市 char *m_district; //区(市区) };
- 友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员
- 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类
16.class和struct区别
C++中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数
- 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的
- class 继承默认是 private 继承,而 struct 继承默认是 public 继承
- class 可以使用模板,而 struct 不能
17.string、字符串
string 是 C++ 中常用的一个类
#include <iostream> #include <string> using namespace std; int main(){ string s1; string s2 = "c plus plus"; string s3 = s2; string s4 (5, 's'); return 0; }
变量 s1 只是定义但没有初始化,编译器会将默认值赋给 s1,默认值是""
,也即空字符串。
变量 s2 在定义的同时被初始化为"c plus plus"
。与C风格的字符串不同,string 的结尾没有结束标志'\0'
变量 s3 在定义的时候直接用 s2 进行初始化,因此 s3 的内容也是"c plus plus"
变量 s4 被初始化为由 5 个's'
字符组成的字符串,也就是"sssss"
字符串的拼接#
string s1 = "first "; string s2 = "second "; string s3 = s1 + s2;
string 字符串的增删改查#
一. 插入字符串#
insert (pos, str);
pos 表示插入字符串下标;str 表示要插入的字符串,
#include <iostream> #include <string> using namespace std; int main(){ string s1, s2, s3; s1 = s2 = "1234567890"; s3 = "aaa"; s1.insert(5, s3); }
结果:
12345aaa67890
二. 删除字符串#
erase (pos, str);
pos 表示要删除子字符串的下标,len 表示要删除子字符串的长度
#include <iostream> #include <string> using namespace std; int main(){ string s1, s2, s3; s1 = s2 = s3 = "1234567890"; s2.erase(5); s3.erase(5, 3); cout<< s1 <<endl; cout<< s2 <<endl; cout<< s3 <<endl; return 0; }
结果:
1234567890
12345
1234590
三. 提取子字符串#
substr (pos, len)
#include <iostream> #include <string> using namespace std; int main(){ string s1 = "first second third"; string s2; s2 = s1.substr(6, 6); cout<< s1 <<endl; cout<< s2 <<endl; return 0; }
结果:
first second third
second
四. 字符串查找#
1) find() 函数#
第一个参数为待查找的子字符串,它可以是 string 字符串,也可以是C风格的字符串。第二个参数为开始查找的位置(下标)
2) rfind() 函数#
最多查找到第二个参数处,如果到了第二个参数所指定的下标还没有找到子字符串,则返回 string::npos
3) find_first_of() 函数#
查找子字符串和字符串共同具有的字符在字符串中首次出现的位置
类和对象的总结
类的成员有成员变量和成员函数两种。
成员函数之间可以互相调用,成员函数内部可以访问成员变量。
私有成员只能在类的成员函数内部访问。默认情况下,class 类的成员是私有的,struct 类的成员是公有的。
可以用“对象名.成员名”、“引用名.成员名”、“对象指针->成员名”的方法访问对象的成员变量或调用成员函数。成员函数被调用时,可以用上述三种方法指定函数是作用在哪个对象上的。
对象所占用的存储空间的大小等于各成员变量所占用的存储空间的大小之和(如果不考虑成员变量对齐问题的话)。
定义类时,如果一个构造函数都不写,则编译器自动生成默认(无参)构造函数和复制构造函数。如果编写了构造函数,则编译器不自动生成默认构造函数。一个类不一定会有默认构造函数,但一定会有复制构造函数。
任何生成对象的语句都要说明对象是用哪个构造函数初始化的。即便定义对象数组,也要对数组中的每个元素如何初始化进行说明。如果不说明,则编译器认为对象是用默认构造函数或参数全部可以省略的构造函数初始化。在这种情况下,如果类没有默认构造函数或参数全部可以省略的构造函数,则编译出错。
对象在消亡时会调用析构函数。
每个对象有各自的一份普通成员变量,但是静态成员变量只有一份,被所有对象所共享。静态成员函数不具体作用于某个对象。即便对象不存在,也可以访问类的静态成员。静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数。
常量对象上面不能执行非常量成员函数,只能执行常量成员函数。
包含成员对象的类叫封闭类。任何能够生成封闭类对象的语句,都要说明对象中包含的成员对象是如何初始化的。如果不说明,则编译器认为成员对象是用默认构造函数或参数全部可以省略的构造函数初始化。
在封闭类的构造函数的初始化列表中可以说明成员对象如何初始化。封闭类对象生成时,先执行成员对象的构造函数,再执行自身的构造函数;封闭类对象消亡时,先执行自身的析构函数,再执行成员对象的析构函数。
const 成员和引用成员必须在构造函数的初始化列表中初始化,此后值不可修改。
友元分为友元函数和友元类。友元关系不能传递。
成员函数中出现的 this 指针,就是指向成员函数所作用的对象的指针。因此,静态成员函数内部不能出现 this 指针。成员函数实际上的参数个数比表面上看到的多一个,多出来的参数就是 this 指针。
18.引用 & 用法
同指针一样,引用能够减少数据的拷贝,提高数据的传递效率
参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。指将一块内存上的数据复制到另一块内存上。
对于像 char、bool、int、float 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组、结构体、对象是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行频繁的内存拷贝可能会消耗很多时间,拖慢程序的执行效率
C/C++禁止在函数调用时直接传递数组的内容,而是强制传递数组指针
对于结构体和对象没有这种限制,调用函数时既可以传递指针,也可以直接传递内容;为了提高效率,建议传递指针
C++ 中,有了一种比指针更加便捷的传递聚合类型数据的方式,那就是引用(Reference)
引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。引用类似于 Windows 中的快捷方式
引用的定义方式类似于指针,只是用&
取代了*
引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据,这有点类似于常量(const 变量)
#include <iostream> using namespace std; int main() { int a = 99; int &r = a; cout << a << ", " << r << endl; cout << &a << ", " << &r << endl; return 0; }
结果:
99, 99
0x28ff44, 0x28ff44
引用在定义时需要添加&
,在使用时不能添加&
,使用时添加&
表示取地址,&
还可以表示位运算中的与运算
由于引用 r 和原始变量 a 都是指向同一地址,所以通过引用也可以修改原始变量中所存储的数据
#include <iostream> using namespace std; int main() { int a = 99; int &r = a; r = 47; cout << a << ", " << r << endl; return 0; }
结果:
47, 47
如果不希望通过引用来修改原始的数据,那么可以在定义时添加 const 限制 const type &name = value;
引用作为函数参数#
一个能够展现按引用传参的优势的例子就是交换两个数的值
#include <iostream> using namespace std; void swap1(int a, int b); void swap2(int *p1, int *p2); void swap3(int &r1, int &r2); int main() { int num1, num2; cout << "Input two integers: "; cin >> num1 >> num2; swap1(num1, num2); cout << num1 << " " << num2 << endl; cout << "Input two integers: "; cin >> num1 >> num2; swap2(&num1, &num2); cout << num1 << " " << num2 << endl; cout << "Input two integers: "; cin >> num1 >> num2; swap3(num1, num2); cout << num1 << " " << num2 << endl; return 0; } //直接传递参数内容 void swap1(int a, int b) { int temp = a; a = b; b = temp; } //传递指针 void swap2(int *p1, int *p2) { int temp = *p1; *p1 = *p2; *p2 = temp; } //按引用传参 void swap3(int &r1, int &r2) { int temp = r1; r1 = r2; r2 = temp; }
结果:
Input two integers: 12 34↙
12 34
Input two integers: 88 99↙
99 88
Input two integers: 100 200↙
200 100
swap1() 直接传递参数的内容,不能达到交换两个数的值的目的,在函数内部有用,但是外部的num1,num2不起作用
swap2() 传递的是指针,能够达到交换两个数的值的目的(指针指的是内存的地址,所以要用&取地址)
swap3() 是按引用传递,能够达到交换两个数的值的目的
按引用传参在使用形式上比指针更加直观。鼓励使用引用,它一般可以代替指针
引用作为函数返回值#
#include <iostream> using namespace std; int &plus10(int &r) { r += 10; return r; } int main() { int num1 = 10; int num2 = plus10(num1); cout << num1 << " " << num2 << endl; return 0; }
运行结果:
20 20
19.继承和派生
被继承的类称为父类或基类,继承的类称为子类或派生类
派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。
1) 当创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能。
2) 当需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员。
#include<iostream> using namespace std; //基类 Pelple class People{ public: void setname(char *name); void setage(int age); char *getname(); int getage(); private: char *m_name; int m_age; }; void People::setname(char *name){ m_name = name; } void People::setage(int age){ m_age = age; } char* People::getname(){ return m_name; } int People::getage(){ return m_age;} //派生类 Student class Student: public People{ public: void setscore(float score); float getscore(); private: float m_score; }; void Student::setscore(float score){ m_score = score; } float Student::getscore(){ return m_score; } int main(){ Student stu; stu.setname("小明"); stu.setage(16); stu.setscore(95.5f); cout<<stu.getname()<<"的年龄是 "<<stu.getage()<<",成绩是 "<<stu.getscore()<<endl; return 0; }
People 是基类,Student 是派生类。Student 类继承了 People 类的成员,同时还新增了自己的成员变量 score 和成员函数 setscore()、getscore()。这些继承过来的成员,可以通过子类对象访问,就像自己的一样。
class Student: public People
这就是声明派生类的语法。class 后面的“Student”是新声明的派生类,冒号后面的“People”是已经存在的基类。在“People”之前有一关键宇 public,用来表示是公有继承。
如果不写,那么默认为 private。
20.三种继承方式
public、protected、private 修饰类的成员#
protected 成员和 private 成员类似,也不能通过对象访问。
但是当存在继承关系时,基类中的 protected 成员可以在派生类中使用,而基类中的 private 成员不能在派生类中使用
public、protected、private 指定继承方式#
不同的继承方式会影响基类成员在派生类中的访问权限。
总结:
1) 基类成员在派生类中的访问权限不得高于继承方式中指定的权限
也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。
2) 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)
3) 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
4) 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。
private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中一般使用 public
#include<iostream> using namespace std; //基类People class People{ public: void setname(char *name); void setage(int age); void sethobby(char *hobby); char *gethobby(); protected: char *m_name; int m_age; private: char *m_hobby; }; void People::setname(char *name){ m_name = name; } void People::setage(int age){ m_age = age; } void People::sethobby(char *hobby){ m_hobby = hobby; } char *People::gethobby(){ return m_hobby; } //派生类Student class Student: public People{ public: void setscore(float score); protected: float m_score; }; void Student::setscore(float score){ m_score = score; } //派生类Pupil class Pupil: public Student{ public: void setranking(int ranking); void display(); private: int m_ranking; }; void Pupil::setranking(int ranking){ m_ranking = ranking; } void Pupil::display(){ cout<<m_name<<"的年龄是"<<m_age<<",考试成绩为"<<m_score<<"分,班级排名第"<<m_ranking<<",TA喜欢"<<gethobby()<<"。"<<endl; } int main(){ Pupil pup; pup.setname("小明"); pup.setage(15); pup.setscore(92.5f); pup.setranking(4); pup.sethobby("乒乓球"); pup.display(); return 0; }
在派生类 Pupil 的成员函数 display() 中,借助基类的 public 成员函数 gethobby() 来访问基类的 private 成员变量 m_hobby,因为 m_hobby 是 private 属性的,在派生类中不可见,所以只能借助基类的 public 成员函数 sethobby()、gethobby() 来访问。
在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问。
改变访问权限#
使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。
using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问
#include<iostream> using namespace std; //基类People class People { public: void show(); protected: char *m_name; int m_age; }; void People::show() { cout << m_name << "的年龄是" << m_age << endl; } //派生类Student class Student : public People { public: void learning(); public: using People::m_name; //将protected改为public using People::m_age; //将protected改为public float m_score; private: using People::show; //将public改为private }; void Student::learning() { cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl; } int main() { Student stu; stu.m_name = "小明"; stu.m_age = 16; stu.m_score = 99.5f; stu.show(); //compile error stu.learning(); return 0; }
理解:
因为派生类可以访问基类的Public和Protected,所以可以通过继承方式在规定其权限范围,只不过需要加用关键字using
show() 函数是 private 属性的,所以int main中的stu.show()无法访问,会报错
21.继承时的名字遮蔽问题
#include<iostream> using namespace std; //基类People class People{ public: void show(); protected: char *m_name; int m_age; }; void People::show(){ cout<<"嗨,大家好,我叫"<<m_name<<",今年"<<m_age<<"岁"<<endl; } //派生类Student class Student: public People{ public: Student(char *name, int age, float score); public: void show(); //遮蔽基类的show() private: 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; } int main(){ Student stu("小明", 16, 90.5); //使用的是派生类新增的成员函数,而不是从基类继承的 stu.show(); //使用的是从基类继承来的成员函数 stu.People::show(); return 0; }
基类 People 和派生类 Student 都定义了成员函数 show(),它们的名字一样,会造成遮蔽
但是,基类 People 中的 show() 函数仍然可以访问,不过要加上类名和域解析符 stu.People::show();
基类成员函数和派生类成员函数不构成重载
22.基类和派生类的构造函数
类的构造函数不能被继承。
在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。
这种矛盾在C++承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。
#include<iostream> using namespace std; //基类People class People{ protected: char *m_name; int m_age; public: People(char*, int); }; People::People(char *name, int age): m_name(name), m_age(age){} //派生类Student class Student: public People{ private: float m_score; public: Student(char *name, int age, float score); void display(); }; //People(name, age)就是调用基类的构造函数 Student::Student(char *name, int age, float score): People(name, age), m_score(score){ } void Student::display(){ cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"。"<<endl; } int main(){ Student stu("小明", 16, 90.5); stu.display(); return 0; }
构造函数的调用顺序#
A类构造函数 --> B类构造函数 --> C类构造函数
派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的
基类构造函数调用规则#
定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);
如果没有默认构造函数,那么编译失败
23.基类和派生类的析构函数
和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
- 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
- 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。
24.多继承(多重继承)详解
C++支持多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。
class D: public A, private B, protected C{ //类D新增加的成员 }
D 是多继承形式的派生类,它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。
多继承下的构造函数#
D(形参列表): B(实参列表), C(实参列表), A(实参列表){ //其他操作 }
先调用 A 类的构造函数,再调用 B 类构造函数,最后调用 C 类构造函数。
多继承形式下析构函数的执行顺序和构造函数的执行顺序相反。
命名冲突#
当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::
#include <iostream> using namespace std; //基类 class BaseA{ public: BaseA(int a, int b); ~BaseA(); public: void show(); protected: int m_a; int m_b; }; BaseA::BaseA(int a, int b): m_a(a), m_b(b){ cout<<"BaseA constructor"<<endl; } BaseA::~BaseA(){ cout<<"BaseA destructor"<<endl; } void BaseA::show(){ cout<<"m_a = "<<m_a<<endl; cout<<"m_b = "<<m_b<<endl; } //基类 class BaseB{ public: BaseB(int c, int d); ~BaseB(); void show(); protected: int m_c; int m_d; }; BaseB::BaseB(int c, int d): m_c(c), m_d(d){ cout<<"BaseB constructor"<<endl; } BaseB::~BaseB(){ cout<<"BaseB destructor"<<endl; } void BaseB::show(){ cout<<"m_c = "<<m_c<<endl; cout<<"m_d = "<<m_d<<endl; } //派生类 class Derived: public BaseA, public BaseB{ public: Derived(int a, int b, int c, int d, int e); ~Derived(); public: void display(); private: int m_e; }; Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){ cout<<"Derived constructor"<<endl; } Derived::~Derived(){ cout<<"Derived destructor"<<endl; } void Derived::display(){ BaseA::show(); //调用BaseA类的show()函数 BaseB::show(); //调用BaseB类的show()函数 cout<<"m_e = "<<m_e<<endl; } int main(){ Derived obj(1, 2, 3, 4, 5); obj.display(); return 0; }
25.虚继承和虚基类详解
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承
//间接基类A class A{ protected: int m_a; }; //直接基类B class B: public A{ protected: int m_b; }; //直接基类C class C: public A{ protected: int m_c; }; //派生类D class D: public B, public C{ public: void seta(int a){ m_a = a; } //命名冲突 void setb(int b){ m_b = b; } //正确 void setc(int c){ m_c = c; } //正确 void setd(int d){ m_d = d; } //正确 private: int m_d; }; int main(){ D d; return 0; }
为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:
void seta(int a){ B::m_a = a; }
这样表示使用 B 类的 m_a。当然也可以使用 C 类的:
void seta(int a){ C::m_a = a; }
虚继承(Virtual Inheritance)#
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
//间接基类A class A{ protected: int m_a; }; //直接基类B class B: virtual public A{ //虚继承 protected: int m_b; }; //直接基类C class C: virtual public A{ //虚继承 protected: int m_c; }; //派生类D class D: public B, public C{ public: void seta(int a){ m_a = a; } //正确 void setb(int b){ m_b = b; } //正确 void setc(int c){ m_c = c; } //正确 void setd(int d){ m_d = d; } //正确 private: int m_d; }; int main(){ D d; return 0; }
这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
必须在虚派生的真实需求出现前就已经完成虚派生的操作。
虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
虚基类成员的可见性#
因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。
不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。
虚继承时的构造函数
在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。
虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。
26.将派生类赋值给基类(向上转型)
类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting)。
将派生类对象赋值给基类对象#
#include <iostream> using namespace std; //基类 class A{ public: A(int a); public: void display(); public: int m_a; }; A::A(int a): m_a(a){ } void A::display(){ cout<<"Class A: m_a="<<m_a<<endl; } //派生类 class B: public A{ public: B(int a, int b); public: void display(); public: int m_b; }; B::B(int a, int b): A(a), m_b(b){ } void B::display(){ cout<<"Class B: m_a="<<m_a<<", m_b="<<m_b<<endl; } int main(){ A a(10); B b(66, 99); //赋值前 a.display(); b.display(); cout<<"--------------"<<endl; //赋值后 a = b; a.display(); b.display(); return 0; }
赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。
这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。
将派生类指针(引用)赋值给基类指针(基类引用)#
编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。
引用和指针并没有本质上的区别
向上转型后通过基类的对象、指针、引用只能访问从基类继承过去的成员(包括成员变量和成员函数),不能访问派生类新增的成员。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix