c++(二)
1. 类和对象
1.1. 封装
封装的意义
- 将属性和行为作为一个整体,表现生活中的事物;
- 将属性和行为加以权限控制
- public -> 公共权限:类内可以访问,类外也可以访问
- protected -> 保护权限:类内可以访问,类外不可以访问,子类可以访问
- private -> 私有权限:类内可以访问,类外不可以访问,子类不可以访问
struct和class的区别
- struct默认权限为公共权限
- class默认权限为私有权限
1.2. 对象的初始化和清理
1.2.1. 构造函数
- 编译器自动调用,完成对象的初始化工作
- 在创建对象时为对象的成员属性赋值
1.2.1.1. 构造函数的分类
无参构造函数
#include<iostream>
#include<string>
using namespace std;class Student
{
private:string mName;int mAge;
public:// 无参构造函数Student() {cout << "调用无参构造函数" << endl;};
};
有参构造函数
#include<iostream>
#include<string>
using namespace std;class Student
{
private:string mName;int mAge;
public:// 有参构造函数Student(string name, int age) {cout << "调用有参构造函数" << endl;mName = name;mAge = age;}string getName(){return mName;}int getAge(){return mAge;}// 析构函数~Student() {};
};
拷贝构造函数
#include<iostream>
#include<string>
using namespace std;class Student
{
private:string mName;int mAge;
public:// 拷贝构造函数Student(const Student &s) {mName = s.mName;mAge = s.mAge;};string getName(){return mName;}int getAge(){return mAge;}// 析构函数~Student() {};
};int main()
{Student s1 = Student("lisi", 23);// 调用拷贝构造函数Student s2 = s1;// 不能使用匿名对象构造拷贝构造方法,编译器会把代码转换为Student s2; 认为重复定义了一个s2的变量。//Student(s2); // 错误的代码
}
1.2.1.2. 构造函数的初始化列表
#include<iostream>
#include<string>
using namespace std;class CDate
{
public:CDate(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};class CGoods
{
public:CGoods(const char* n, int a, double p, int year, int month, int day):_date(year, month, day), // 这行代码相当于 CDate _date(year, month, day),即指定有参构造函数构造CDate对象,_amount(a), _price(p){strcpy(_name, n); // _name成员变量必须要放到构造函数体中初始化// _date = CDate(year, month, day) // 如果写在构造函数体中,相当于 CDate _date; _date = CDate(year, month, day),// 即先构造一个CDate对象(默认使用无参构造函数),然后再给这个对象赋值。// 但是CDate并没有默认构造函数,因此在编译的时候就会报错。 }
private:char _name[20];int _amount;double _price;CDate _date; // 成员对象
};#if 0
int main()
{CGoods good("沙发", 1, 12000, 2022, 1, 1);
}
#endif // 0
1.2.1.3. 构造函数的成员初始化顺序
成员变量的初始化顺序只与其在类中被声明的顺序有关,而与在初始化列表的顺序无关。
//
// main.cpp
// 数据成员的初始化顺序问题
//
// Created by apple on 2023/10/7.
//#include <iostream>
#include <string>using namespace std;class Point
{
public:Point(int ix):_iy(ix),_ix(_iy)// 会先初始化_ix,再初始化_iy{cout << "Point(int,int)" << endl;}void print(){cout << _ix << "\t" << _iy << endl;}private:int _ix;int _iy;
};int main(int argc, const char * argv[]) {Point p(3);p.print();return 0;
}
打印结果如下:
Point(int,int)
1 3
1.2.1.4. 拷贝构造函数的调用时机(重点)
使用一个已经创建完成的对象来初始化一个新对象
Student s1 = Student("lisi", 23);
// 调用拷贝构造函数
Student s2 = s1;
值传递的方式给函数参数传值
#include <iostream>
#include <string>
using namespace std;class Student
{
public:int age;int* p_height;// 构造函数// 无参构造函数Student() {cout << "调用无参构造函数" << endl;};// 有参构造函数Student(int a, int height) {cout << "调用有参构造函数" << endl;age = a;p_height = new int(height);}// 自定义拷贝构造函数Student(const Student &s) {cout << "调用自定义的拷贝构造函数" << endl;age = s.age;p_height = new int(*s.p_height);};//析构函数~Student() {// 在析构函数中可以做一些资源释放的工作if (p_height != nullptr) {delete p_height;p_height = nullptr;}cout << "调用析构函数" << endl;};
};void func(Student s) // 值传递
{cout << s.age <<"\t"<< *s.p_height << endl;
}int main()
{// 显式构造对象Student s = Student(23, 170);// 括号法构造对象Student s1 (23, 170);// 以值的方式将对象传递给函数参数,会调用拷贝构造函数func(s);
}
输出结果如下:
调用有参构造函数调用有参构造函数调用自定义的拷贝构造函数23 170调用析构函数调用析构函数调用析构函数
以值方式返回局部对象
#include<iostream>
#include<string>
using namespace std;class Student
{
public:int age;int* p_height;// 构造函数// 无参构造函数Student() {cout << "调用无参构造函数" << endl;};// 有参构造函数Student(int a, int height) {cout << "调用有参构造函数" << endl;age = a;p_height = new int(height);}// 自定义拷贝构造函数Student(const Student &s) {cout << "调用自定义的拷贝构造函数" << endl;age = s.age;p_height = new int(*s.p_height);};//析构函数~Student() {// 在析构函数中可以做一些资源释放的工作if (p_height != nullptr) {delete p_height;p_height = nullptr;}cout << "调用析构函数" << endl;};
};Student func1()
{Student s = Student(21, 180);// 以值的方式返回局部对象,会调用拷贝函数return s;
}int main()
{Student s = func1();
}
1.2.1.5. 拷贝构造函数的形式是固定的
拷贝构造函数形式:类名(const 类名 & rhs)
是否可以去掉引用符号?即将其改为 类名(const 类名 rhs)
,答案是不可以,因为会产生构造函数无穷递归调用的情况。
当执行 Point pt2 = pt 时,会调用拷贝构造函数,然后拷贝构造函数的形参会初始化,初始化形参又会调用拷贝构造函数,这样无穷递归下去,直到栈溢出。
是否可以去掉const关键字?即将其改为 类名( 类名 & rhs)
,答案是不可以,因为非const引用不能绑定右值。
假设 func()函数的返回值是一个 Point 对象,当执行 Point pt2 = func() 时,会调用拷贝构造函数,然而当给拷贝构造函数传递参数时, 如果没有 const,Point & rp = func() 是不正确的,因为 func()是一个临时对象,是右值,非const引用不能绑定右值。
1.2.1.6. 构造函数的调用规则
- 如果用户定义了有参构造函数,c++不再提供默认的无参构造函数,但是会提供默认的拷贝构造函数;
- 如果用户定义了拷贝构造函数,c++不再提供其他构造函数。
1.2.1.7. 构造函数的调用方式
总结:
Student("zhaoliu", 21); // 匿名对象
Student s1("zhangsan", 20); // 括号法
Student s2 = Student("lisi", 18); // 显式构造
Student s3 = {"wangwu", 19}; // 隐式转换
括号法
#include<iostream>
#include<string>
using namespace std;class Student
{
public:// 无参构造函数Student() {cout << "调用无参构造函数" << endl;};// 有参构造函数Student(string n, int a) {cout << "调用有参构造函数" << endl;name = n;age = a;}// 拷贝构造函数Student(const Student &s) {name=s.name;age = s.age;};string getName(){return name;}int getAge(){return age;}// 析构函数~Student() {};private:string name;int age;
};
int main()
{// 实例化对象:括号法// 调用无参构造方法时,不能加"()",因为编译器认为这是一个函数的声明Student s1;// 调用有参构造方法Student s2("zhansan", 22);cout << s2.getName() << "\t"<<s2.getAge() << endl;// 匿名对象,当前行执行结束之后,系统会立即收掉匿名对象Student("wangwu", 23); }
显示法
#include<iostream>
#include<string>
using namespace std;class Student
{
public:// 无参构造函数Student() {cout << "调用无参构造函数" << endl;};// 有参构造函数Student(string n, int a) {cout << "调用有参构造函数" << endl;name = n;age = a;}// 拷贝构造函数Student(const Student &s) {name=s.name;age = s.age;};string getName(){return name;}int getAge(){return age;}// 析构函数~Student() {};private:string name;int age;
};
int main()
{// 实例化对象:显示法Student s3 = Student("lisi", 23);cout << s3.getName() << "\t" << s3.getAge() << endl;// 匿名对象,当前行执行结束之后,系统会立即收掉匿名对象Student("wangwu", 23);
}
隐式转换法
#include<iostream>
#include<string>
using namespace std;class Student
{
public:// 无参构造函数Student() {cout << "调用无参构造函数" << endl;};// 有参构造函数Student(string n, int a) {cout << "调用有参构造函数" << endl;name = n;age = a;}// 拷贝构造函数Student(const Student &s) {name=s.name;age = s.age;};string getName(){return name;}int getAge(){return age;}// 析构函数~Student() {};private:string name;int age;
};
int main()
{// 实例化对象:隐式转换法,如果有参构造函数只有一个参数,就不需要加"{}",但是可能会导致问题,不推荐。Student s5 = { "zhaoliu", 26 };
}
1.2.1.8. 带explicit 关键字的构造函数
不带 explicit 关键字
class MyClass {
public:MyClass(int x) {value = x;}
private:int value;
};void func(MyClass obj) {// do something
}int main() {MyClass a = 10; // 隐式调用MyClass(int)构造函数func(10); // 隐式将int转换为MyClass对象return 0;
}
带 explicit 关键字
class MyClass {
public:explicit MyClass(int x) {value = x;}
private:int value;
};void func(MyClass obj) {// do something
}int main() {MyClass a = 10; // 错误:不能隐式转换int到MyClassMyClass b(10); // 正确:显式调用构造函数func(10); // 错误:不能隐式转换int到MyClassfunc(MyClass(10)); // 正确:显式创建MyClass对象return 0;
}
1.2.2. 析构函数
- 编译器自动调用,完成对象的清理工作
- 在对象销毁前执行一些清理工作
1.2.2.1. 析构函数的调用时机(重点)
- 栈对象生命周期结束时,会自动调用析构函数;
- 全局对象在main函数退出时,会自动调用析构函数;
- (局部)静态对象在main函数退出时,会自动调用析构函数;
- 堆对象在执行delete时,会自动调用析构函数。
1.2.3. 浅拷贝和深拷贝(重点)
#include<iostream>
#include<string>
using namespace std;class Student
{
public:int age;int* p_height;// 无参构造函数Student() {cout << "调用无参构造函数" << endl;};// 有参构造函数Student(int a, int height) {cout << "调用有参构造函数" << endl;age = a;// 在堆内存中创建一块空间p_height = new int(height);}// 自定义拷贝构造函数Student(const Student &s) {cout << "调用自定义的拷贝构造函数" << endl;age = s.age;// p_height = s.p_height 编译器默认实现的就是这行代码p_height = new int(*s.p_height); // 自定义实现深拷贝};//析构函数~Student() {// 在析构函数中可以做一些资源释放的工作if (p_height != nullptr) {// 释放内存delete p_height;p_height =nullptr;}cout << "调用析构函数" << endl;};
};void test()
{Student s1(18, 170);cout << "s1对象的年龄:" << s1.age << ",s1对象的身高:" << *s1.p_height << endl;// Student s2 = s1;Student s2(s1);// 如果没有自定义拷贝构造函数,默认是浅拷贝,当s2对象执行析构函数之后,p_height所指向的内存就被释放了// 然后,当s1对象执行析构函数时,再释放p_height的内存就是非法操作了,因此会报错,解决的办法就是深拷贝。// 通过自定义拷贝构造函数来实现深拷贝cout << "s2对象的年龄:" << s2.age << ",s2对象的身高:" << *s2.p_height << endl;
}
int main()
{test();
}
1.2.4. 赋值函数
class Computer
{
public:Computer(const char *brand, double price): _brand(new char[strlen(brand) + 1]()), _price(price){cout << "Computer(const char *, double)" << endl;}~Computer(){cout << "~Computer()" << endl;delete [] _brand;_brand = nullptr;}Computer &Computer::operator=(const Computer &rhs){if(this != &rhs) //1、自复制{delete [] _brand; //2、释放左操作数_brand = nullptr;_brand = new char[strlen(rhs._brand) + 1](); //3、深拷贝strcpy(_brand, rhs._brand);_price = rhs._price;}return *this; //4、返回*this}
private:char *_brand;double _price;
};
- 返回类型不必须是类类型:可以是其他类型,但一般没有这种需求。
- 返回值不一定是引用:可以返回值类型,但这样会引入不必要的拷贝。
class MyClass {
public:MyClass operator=(const MyClass& other) {if (this != &other) {// 执行赋值操作}return *this; // 这里会涉及一次拷贝}
};
1.2.5. 初始化列表
#include<iostream>
#include<string>
using namespace std;class Student
{
public:string s_name;int s_age;// 初始化列表, s_name(name)相当于string s_name=name, s_age(a)相当于int s_age = a;Student(string name, int a) :s_name(name), s_age(a){cout << "调用有参构造函数" << endl;}
};int main()
{// 括号法创建对象Student s1 ("zhangsan", 18);// 显示法创建对象Student s2 = Student("lisi", 19);cout << "s_name:" << s1.s_name << endl;cout << "s_age:" << s1.s_age << endl;
}
1.2.6. const成员变量(重点)
const成员变量必须要放在初始化列表中进行初始化。
class Book{
public:Book( int s );
private:int i;const int j;int &k;Book::Book( int s ): i(s), j(s), k(s){}
1.2.7. 引用成员变量(重点)
引用成员变量也必须要放在初始化列表中进行初始化。
1.2.8. 类对象作为类成员
结论:当其他类对象作为本类成员,构造函数先构造其他类对象,再构造本对象;析构的顺序与构造相反。
1.2.9. 静态成员变量
- 所有对象共享该成员变量
- 在编译阶段分配内存
- 类内声明,类外初始化
#include<iostream>
#include<string>
using namespace std;class Person
{
public:// 静态成员变量,类内声明static int pAge;
};
// 类外初始化,注意初始化的语法
int Person::pAge = 18;void test()
{// 静态成员变量可以直接通过类名访问cout << Person::pAge << endl;
}int main()
{test();
}
1.2.10. 静态成员函数
- 静态成员函数内部只能访问静态成员属性和静态成员函数;
- 静态成员函数的参数列表中没有隐含的this指针。
#include<iostream>
#include<string>
using namespace std;class Person
{
public:// 静态成员函数,类内声明static void func();
};
// 类外初始化,注意初始化的语法
void Person::func() {cout << "Person类的静态成员方法" << endl;
}int main()
{Person::func();
}
1.2.11. 虚函数(*)
virtual
#include <iostream>
#include <typeinfo>using namespace std;class Base
{
public:Base(int data = 10):ma(data){} // 构造函数virtual void show() { cout << "Base::show()" << endl; }void show(int) { cout << "Base::show(int)" << endl; }
private:int ma;
};
class Derive :public Base
{
public:Derive(int data=20):Base(data), mb(data){}void show() { cout << "Derive::show()" << endl; }
private:int mb;
};#if 1
int main()
{Derive d(50);Base *pb = &d;pb->show(); // 动态绑定 Derive::show()pb->show(10); // 静态绑定 Base::show(int)cout << sizeof(Base) << endl;cout << sizeof(Derive) << endl;cout << typeid(pb).name() << endl; // 指针的类型 class Base*// 如果Base中没有虚函数,*pb识别的就是编译时期的类型,即Base// 如果Base中有虚函数,*pb识别的就是运行时期的类型 即RTTI指针指向的类型cout << typeid(*pb).name() << endl; // 指针指向的类型 class Derive
}
#endif
- 特性1:如果一个类里面定义了虚函数,那么在编译阶段,编译器就会给这个类产生唯一的一个vftable(虚函数表)。虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的只读数据区。
- 特性2:如果一个类里面定义了虚函数,那么这个类的内存会多存储一个指向虚函数地址的指针(vfptr)。
- 特性3:如果派生类中的方法和继承过来的某个方法在返回值、函数名、参数列表上都相同,而且基类中的该方法是虚函数,那么派生类的这个方法会被自动处理成虚函数。
1.2.11.1. 静态绑定
类中的普通函数在编译时就确定了地址了,即为静态绑定。c++的重载函数就是静态绑定,因为在编译的时候就需要确定调用的是哪个函数。
1.2.11.2. 动态绑定
类中的虚函数在编译时不能确定函数地址,即为动态绑定。另外,必须使用指针(引用)的方式调用虚函数才会产生动态绑定。
Base &rb1 = b;
rb1.show();
Base &rb2 = d;
rb2.show();Derive *pd1 = &d;
pd1->show();
由对象直接调用虚函数不会产生动态绑定,因为可以确定是哪个对象调用的。
Base b;
b.show(); // 静态绑定
Derive d;
d.show(); // 静态绑定
1.2.11.3. 哪些函数不能成为虚函数
首先说一下虚函数依赖
- 虚函数要能产生地址,存储在虚函数表中;
- 对象必须存在,只能通过对象来找到虚函数表的地址,进而找到虚函数地址
从虚函数依赖可知,以下函数不能成为虚函数
- 构造函数:要执行完构造函数后才能创建对象,因此在调用构造函数时还没有对象,不能成为虚函数。且在构造函数中调用虚函数也不会发生静态绑定;
- static成员函数:跟对象没有关系,也是不行的。
1.2.11.4. 虚析构函数
1.3. c++对象模型和this指针
1.3.1. 成员变量和成员函数内存空间归属
- 非静态成员变量占用的内存空间属于类对象;
- 静态成员变量、静态成员函数、非静态成员函数占用的内存空间都不属于类对象;
- 空对象占用的空间大小为1个字节。
1.3.2. this指针的使用
this指针本质上是一个指针常量,即指向的对象是不可以再更改的,但是指向的对象的值是可以修改的。
每一个成员函数都拥有一个隐含的this指针,这个this指针作为函数的第一个参数。
#include<iostream>
#include<string>using namespace std;const int NAME_LEN = 20;class CGoods
{
public:// 编译的时候会在第一个参数的位置添加this指针,void show(CGood* this);void show(){cout << "show方法" << endl;}// 编译的时候会在第一个参数的位置添加this指针,void setPrice(CGood* this, double price)void setPrice(double price){_price = price;}
private:char _name[NAME_LEN];double _price;int _amount;
};#if 1
int main()
{CGoods good;good.show();
}
#endif
#include<iostream>
#include<string>using namespace std;class Person
{
public:int age;Person(int age){this->age = age;}// 引用作为函数的返回值 Person &p = personPerson& addAge(int age){// this指针this->age += age;// this指针的解引用就是当前对象return *this;}
};void test()
{Person p(10);p.addAge(10).addAge(10).addAge(10);cout << p.age << endl;
}
int main()
{test();
}
1.4. C++ this和*this的区别
- this返回的是当前对象的地址(指向当前对象的指针);
- *this返回的是当前对象的克隆和本身(若返回类型是A,则是克隆,若返回类型是A&,则是本身);
std::unique_ptr<PaddleClasModel> PaddleClasModel::Clone() const {std::unique_ptr<PaddleClasModel> clone_model =utils::make_unique<PaddleClasModel>(PaddleClasModel(*this));clone_model->SetRuntime(clone_model->CloneRuntime());return clone_model;
PaddleClasModel(*this)调用的是默认的拷贝构造函数
class PaddleClasModel {
public:PaddleClasModel(const PaddleClasModel& other);
};
1.4.1. 常对象和常函数
常函数
- 成员函数后加const即为常函数
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象
- 声明对象前加const即为常对象
- 常对象只能调用常函数。常方法,编译器添加的是 const 修饰的 this 指针
- 非常对象也可以调用常函数
#include<iostream>
#include<string>using namespace std;class Person
{
public:int age;// 成员属性加了mutable关键字,即使在常函数中也是可以更改name属性的值的mutable string name;Person(int age, string name){this->age = age;this->name = name;}// 常函数,使用const修饰,本质上就是const Person * const p;因此既不可以更改对象,也不可以更改对象的属性值void func() const{cout << "调用常函数" << endl;//this->age = 20; 错误,常函数不可以修改成员属性// name属性是可以修改的this->name = "wangwu";}void func2(){cout << "调用普通函数" << endl;}};void test()
{Person p(10, "lisi");p.func();cout << p.name << endl;// 常对象const Person p2(20, "zhaoliu");//p2.func2(); 那么p2.func();}
int main()
{test();
}
1.4.2. 空指针访问成员函数
#include<iostream>
#include<string>
using namespace std;class Person
{
public:int age;void showClass(){cout << "this is Person class" << endl;}void showPersonAge(){// age 默认指的是this->age既然this都是空了,那么访问this->age肯定就会报错了cout << "age = " << age << endl;}
};void test()
{Person *p = nullptr;//p->showClass(); // 正确//p->showPersonAge(); // 报错
}int main()
{test();
}
1.5. 类和对象代码应用实践
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>using namespace std;class String
{
public:String(const char *str=nullptr){if (str != nullptr){m_data = new char[strlen(str) + 1];strcpy(this->m_data, str);}else{m_data = new char[1];*m_data = '\0';}}// 拷贝构造函数String(const String& str){m_data = new char[strlen(str.m_data) + 1];strcpy(m_data, str.m_data);}// 析构函数~String(){delete[] m_data;m_data = nullptr;}// 赋值构造函数String& operator=(const String & other){if (this == &other){return *this;}delete[] m_data; // 先释放之前指向堆内存的空间m_data = new char[strlen(other.m_data) + 1]; // 再重新申请一块堆空间,c语言的字符串最后有一个'\0'字符,因此需要多一个字符的长度strcpy(this->m_data, other.m_data); // c语言的strcpy函数,再将other的m_data值拷贝到堆空间return *this;}char* m_data;
};#if 1
int main()
{String s1; // 调用无参构造函数String s2("hello"); // 调用有参构造函数 String s3(s2); // 调用拷贝构造函数s3 = s1; // 调用赋值构造函数
}
#endif
1.6. 指向类成员的指针
#include <iostream>using namespace std;class Test
{
public:int mb;static int si;void func() { cout << "call Test::func" << endl; }static void static_func() { cout << "call Test::static_func" << endl; }
};int Test::si = 20;int main()
{Test t1;int Test::*p = &Test::mb; // 如果写成int *p = &Test::mb; 会报错,无法从 int Test::* 转换成 int *// 指针指向普通成员变量,脱离对象访问成员是没有意义的,因此,在访问p时必须加上对象,不能直接是*p=20t1.*p = 20;cout << t1.mb << endl;// 指针指向静态成员变量,这里就可以这样写了int *p1 = &Test::si;*p1 = 30;cout << Test::si << endl;// 指针指向普通成员方法void(Test::*func)() = &Test::func; // 如果写成void(*func)() = &Test::func; 会报错,无法从 void (__thiscall Test::*)(void)转换成void (__cdecl *)(void)(t1.*func)();// 指针指向静态成员方法void(*static_func)() = &Test::static_func;(*static_func)();}
1.7. 对象数组
1.8. 友元
友元提供了另一访问类的私有成员的方案
1.8.1. 全局函数做友元
#include<iostream>
#include<string>using namespace std;class Building
{// 全局函数做友元,告诉编译器,全局函数goodGay是Building类的好朋友,可以访问类中的私有内容friend void goodGay(Building building);
public:string m_SittingRoom;Building(){this->m_SittingRoom = "客厅";this->m_BedRom = "卧室";}
private:string m_BedRom;
};void goodGay(Building building)
{cout << "好基友正在访问:" << building.m_SittingRoom << endl;cout << "好基友正在访问:" << building.m_BedRom << endl;
}int main()
{Building building;goodGay(building);
}
1.8.2. 类做友元
#include<iostream>
#include<string>using namespace std;class Building;class GoodGay
{
public:// 这里只是声明了构造方法GoodGay();// 这里只是声明了成员函数void visit();
private:// 成员变量Building *building;
};
class Building
{// 友元类,告诉编译器GoodGay类可以访问Building类中的私有内容friend class GoodGay;
public:// 声明构造方法Building();public:// 公共的成员变量string m_sittingRoom;
private:// 私有的成员变量string m_bedroom;
};
// 在类的外部定义Building构造函数
Building::Building()
{this->m_bedroom = "卧室";this->m_sittingRoom = "客厅";
}
// 在类的外部定义GoodGay构造函数
GoodGay::GoodGay() {building = new Building();
}
// 在类的外部定义visit方法
void GoodGay::visit()
{cout << "好基友正在访问" << building->m_bedroom << endl;cout << "好基友正在访问" << building->m_sittingRoom << endl;
}int main()
{GoodGay goodGay = GoodGay();goodGay.visit();
}
1.8.3. 成员函数做友元
#include<iostream>
#include<string>using namespace std;class Building;class GoodGay
{
public:// 这里只是声明了构造方法GoodGay();// 这里只是声明了成员函数void visit();
private:// 成员变量Building *building;
};
class Building
{// 告诉编译器GoodGay类中的成员函数visit可以访问Building类中的私有内容friend void GoodGay::visit();
public:// 声明构造方法Building();public:// 公共的成员变量string m_sittingRoom;
private:// 私有的成员变量string m_bedroom;
};
// 在类的外部定义Building构造函数
Building::Building()
{this->m_bedroom = "卧室";this->m_sittingRoom = "客厅";
}
// 在类的外部定义GoodGay构造函数
GoodGay::GoodGay() {// 无参构造函数"()"可以省略//building = new Building;building = new Building();}
// 在类的外部定义visit方法
void GoodGay::visit()
{cout << "好基友正在访问" << building->m_bedroom << endl;cout << "好基友正在访问" << building->m_sittingRoom << endl;
}int main()
{GoodGay goodGay = GoodGay();goodGay.visit();
}
1.9. 运算符重载
注意事项:
- 对于内置的数据类型的表达式的运算符是不可能改变的;
- 不要滥用运算符重载
1.9.1. 算术运算符重载
1.9.1.1. 加号运算符重载
#include<iostream>
#include<string>using namespace std;class Person
{
public:int m_a;int m_b;// 使用成员函数重载+运算符//Person operator+(Person &p)//{// Person temp;// temp.m_a = m_a + p.m_a;// temp.m_b = m_b + p.m_b;// return temp;//}
};// 使用全局函数重载+运算符
Person operator+(Person &p1, Person &p2)
{Person temp;temp.m_a = p1.m_a + p2.m_a;temp.m_b = p1.m_b + p2.m_b;return temp;
}// 运算符重载的函数重载
Person operator+(Person &p1, int num)
{Person temp;temp.m_a = p1.m_a + num;temp.m_b = p1.m_b + num;return temp;
}
int main()
{// 使用括号法调用无参构造函数不能加括号,因此是Person p1,而不是Person p1()Person p1;p1.m_a = 10;p1.m_b = 10;Person p2;p2.m_a = 10;p2.m_b = 10;Person p3 = p1 + p2;cout << "p3.m_a = " << p3.m_a << "\t" << "p3.m_b = " << p3.m_b << endl;Person p4 = p1 + 100;cout << "p4.m_a = " << p4.m_a << "\t" << "p4.m_b = " << p4.m_b << endl;
}
1.9.1.2. 左移运算符重载
作用:
- 输出自定义对象的成员变量;
- 只能使用全局函数重载版本;
- 如果要输出对象的私有成员,可以配合友元一起使用。
#include<iostream>
#include<string>using namespace std;class Person
{// 全局函数做友元,可以访问类中的私有属性friend ostream & operator<<(ostream &out, Person &p);
private:int m_a;int m_b;
public:void setA(int a){m_a = a;}void setB(int b){m_b = b;}
};// 使用全局函数重载+运算符
ostream & operator<<(ostream &out, Person &p)
{out << "p.m_a = " << p.m_a << ", p.m_b = " << p.m_b;return out;
}int main()
{// 使用括号法调用无参构造函数不能加括号,因此是Person p1,而不是Person p1()Person p1;p1.setA(10);p1.setB(10);cout << p1 << endl;
}
1.9.1.3. 递增运算符重载
#include<iostream>
#include<string>using namespace std;class MyInteger
{// 全局函数做友元,可以访问类中的私有属性friend ostream & operator<<(ostream &out, MyInteger myint);
private:int m_a;
public:MyInteger(){m_a = 1;}// 重载前置++运算符,这里必须返回引用,即同一个对象MyInteger& operator++(){++m_a;return *this;}// 重载后置++运算符MyInteger operator++(int) //int代表占位参数{MyInteger temp = *this; // 先保存当前对象 *this 就表示当前对象m_a++; // 然后再让对象中的m_a的值自增return temp;}
};// 使用全局函数重载<<运算符
ostream & operator<<(ostream &out, MyInteger myint)
{out << "myint.m_a = " << myint.m_a;return out;
}int main()
{MyInteger myint;cout << ++myint << endl;cout << myint << endl;cout << myint++ << endl;cout << myint << endl;
}
1.9.1.4. 赋值运算符重载
#include<iostream>
#include<string>using namespace std;class Person
{
public:int *m_age;
public:Person(int age){// new int 返回的是int类型的指针m_age = new int(age);}~Person(){if (m_age != nullptr){delete m_age;m_age = nullptr;}}// 重载赋值运算符Person& operator=(Person &p){if (m_age != nullptr) {delete m_age; // 释放m_age的内存m_age = new int(*p.m_age); // 重新拷贝一份,放在堆内存,在拷贝之前需要将this指针指向的对象的m_age属性的空间给释放,防止野指针return *this; // 为了能够链式调用,需要返回对象的引用}}};int main()
{Person p1(10);Person p2(20);Person p3(30);// 默认是浅拷贝,在析构函数中清空内存就会存在问题,因此需要手动改为深拷贝p3 = p2 = p1;cout << "p1的年龄为:" << *p1.m_age << endl;cout << "p2的年龄为:" << *p2.m_age << endl;cout << "p3的年龄为:" << *p3.m_age << endl;}
1.9.1.5. new&delete运算符重载
void* operator new(size_t size)
void operator delete(void* ptr)
#include<iostream>
#include<string>using namespace std;void* operator new(size_t size) // 参数必须是size_t(unsigned long long),返回值必须是void*。是静态成员函数
{cout << "调用了全局重载的new:" << size << "字节。\n";// 申请内存void* ptr = malloc(size);cout << "申请到的内存的地址是:" << ptr << endl;return ptr;
}void operator delete(void* ptr) // 参数必须是void *,返回值必须是void。是静态成员函数
{cout << "调用了全局重载的delete。\n";// 判断是否是空指针if (ptr == 0) return; // 对空指针delete是安全的。free(ptr); // 释放内存。
}class CGirl // 超女类CGirl。
{
public:int m_bh; // 编号。int m_xw; // 胸围。CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数CGirl()\n"; }~CGirl() { cout << "调用了析构函数~CGirl()\n"; }void* operator new(size_t size){cout << "调用了类的重载的new:" << size << "字节。\n";// 申请内存void* ptr = malloc(size);cout << "申请到的内存的地址是:" << ptr << endl;return ptr;}void operator delete(void* ptr) // 参数必须是void *,返回值必须是void。{cout << "调用了类的重载的delete。\n";// 判断是否是空指针if (ptr == 0) return; // 对空指针delete是安全的。free(ptr); // 释放内存。}
};int main()
{// 会调用全局重载函数newint* p1 = new int(3);// 会调用全局重载函数deletedelete p1;CGirl* p2 = new CGirl(3, 8);cout << "p2的地址是:" << p2 << "编号:" << p2->m_bh << ",胸围:" << p2->m_xw << endl;delete p2;
}
1.10. 类的自动类型转换
#include<iostream>
#include<string>using namespace std;class CGirl // 超女类CGirl。
{
public:int m_bh; // 编号。string m_name; // 姓名。double m_weight; // 体重,单位:kg。// 默认构造函数。CGirl() { m_bh = 0; m_name.clear(); m_weight = 0; cout << "调用了CGirl()\n"; }// 自我介绍的方法。void show() { cout << "bh=" << m_bh << ",name=" << m_name << ",weight=" << m_weight << endl; }// 关闭自动类型转换,但是可以显式转换explicit CGirl(int bh) { m_bh = bh; m_name.clear(); m_weight = 0; cout << "调用了CGirl(int bh)\n"; }//CGirl(double weight) { m_bh = 0; m_name.clear(); m_weight = weight; cout << "调用了CGirl(double weight)\n"; }
};
int main()
{//CGirl g1(8); // 常规的写法。//CGirl g1 = CGirl(8); // 显式转换。//CGirl g1 = 8; // 隐式转换。CGirl g1; // 创建对象。g1 = (CGirl)8; // 隐式转换,用CGirl(8)创建临时对象,再赋值给g。//CGirl g1 = 8.7; // 隐式转换。//g1.show();
}
1.11. 继承
1.11.1. 继承的基本语法
class 派生类名:[继承方式]基类名
1.11.2. 继承方式
- public
- protected
- private
默认是private。不管继承方式如何,基类中的private成员在派生类中始终不能使用。
1.11.3. 继承中构造和析构顺序
- 创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数;
- 销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。如果手工调用派生类的析构函数,也会调用基类的析构函数
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。class A { // 基类
public:int m_a;
private:int m_b;
public:A() : m_a(0), m_b(0) // 基类的默认构造函数。{cout << "调用了基类的默认构造函数A()。\n";}A(int a, int b) : m_a(a), m_b(b) // 基类有两个参数的构造函数。{cout << "调用了基类的构造函数A(int a,int b)。\n";}A(const A &a) : m_a(a.m_a + 1), m_b(a.m_b + 1) // 基类的拷贝构造函数。{cout << "调用了基类的拷贝构造函数A(const A &a)。\n";}// 显示基类A全部的成员。void showA() { cout << "m_a=" << m_a << ",m_b=" << m_b << endl; }
};class B :public A // 派生类
{
public:int m_c;B() : m_c(0), A() // 派生类的默认构造函数,指明用基类的默认构造函数(不指明也无所谓)。顺序也无所谓{cout << "调用了派生类的默认构造函数B()。\n";}B(int a, int b, int c) : A(a, b), m_c(c) // 指明用基类的有两个参数的构造函数。{cout << "调用了派生类的构造函数B(int a,int b,int c)。\n";}B(const A& a, int c) :A(a), m_c(c) // 指明用基类的拷贝构造函数。{cout << "调用了派生类的构造函数B(const A &a,int c) 。\n";}// 显示派生类B全部的成员。void showB() { cout << "m_c=" << m_c << endl << endl; }
};int main()
{B b1; // 将调用基类默认的构造函数。b1.showA(); b1.showB();B b2(1, 2, 3); // 将调用基类有两个参数的构造函数。b2.showA(); b2.showB();A a(10, 20); // 创建基类对象。B b3(a, 30); // 将调用基类的拷贝造函数。b3.showA(); b3.showB();
}
注意事项:
- 如果没有指定基类构造函数,将使用基类的默认构造函数。如果基类没有默认构造函数,将报错;
- 可以用初始化列表指明要使用的基类构造函数;
- 基类构造函数负责初始化被继承的数据成员,派生类构造函数主要用于初始化新增的数据成员;
- 派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数;
1.11.4. 继承中同名成员处理方式
- 子类对象可以直接访问到子类中同名成员;
- 子类对象加作用域可以访问到父类同名成员;
- 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数。
1.11.5. 继承同名静态成员处理方式
- 访问子类同名成员,直接访问即可;
- 访问父类同名成员,需要加作用域。
1.11.6. 多继承语法
class 子类: 继承方式 父类1, 继承方式 父类2 ...
c++实际开发中不建议用多继承
1.12. 多态
1.12.1. 背景
通过基类只能访问派生类的成员变量,但是不能访问派生类的成员函数。
1.12.2. 虚函数
为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)。使用虚函数非常简单,只需要在函数声明前面增加 virtual 关键字。
注意事项:
- 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加;
- 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数;
- 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员;
- 构造函数不能是虚函数;
- 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。
#include <iostream>
using namespace std;//军队
class Troops {
public:// 基类设置为虚函数virtual void fight() { cout << "Strike back!" << endl; }
};//陆军
class Army : public Troops {
public:void fight() { cout << "--Army is fighting!" << endl; }
};
//99A主战坦克
class _99A : public Army {
public:void fight() { cout << "----99A(Tank) is fighting!" << endl; }
};
//武直10武装直升机
class WZ_10 : public Army {
public:void fight() { cout << "----WZ-10(Helicopter) is fighting!" << endl; }
};
//长剑10巡航导弹
class CJ_10 : public Army {
public:void fight() { cout << "----CJ-10(Missile) is fighting!" << endl; }
};//空军
class AirForce : public Troops {
public:void fight() { cout << "--AirForce is fighting!" << endl; }
};
//J-20隐形歼击机
class J_20 : public AirForce {
public:void fight() { cout << "----J-20(Fighter Plane) is fighting!" << endl; }
};
//CH5无人机
class CH_5 : public AirForce {
public:void fight() { cout << "----CH-5(UAV) is fighting!" << endl; }
};
//轰6K轰炸机
class H_6K : public AirForce {
public:void fight() { cout << "----H-6K(Bomber) is fighting!" << endl; }
};int main() {// 基类指针Troops* p = new Troops;p->fight();//陆军p = new Army;p->fight();p = new _99A;p->fight();p = new WZ_10;p->fight();p = new CJ_10;p->fight();//空军p = new AirForce;p->fight();p = new J_20;p->fight();p = new CH_5;p->fight();p = new H_6K;p->fight();return 0;
}
1.12.3. 纯虚函数
语法格式:virtual 返回值类型 函数名 (函数参数) = 0;
纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。
包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
#include <iostream>
using namespace std;//线
class Line {
public:Line(float len);virtual float area() = 0;virtual float volume() = 0;
protected:float m_len;
};
//类外定义构造函数
Line::Line(float len) : m_len(len) { }//矩形,继承线类,也是一个抽象类,不能实例化对象
class Rec : public Line {
public:Rec(float len, float width);float area();
protected:float m_width;
};
Rec::Rec(float len, float width) : Line(len), m_width(width) { }
float Rec::area() { return m_len * m_width; }//长方体
class Cuboid : public Rec {
public:Cuboid(float len, float width, float height);float area();float volume();
protected:float m_height;
};
Cuboid::Cuboid(float len, float width, float height) : Rec(len, width), m_height(height) { }
float Cuboid::area() { return 2 * (m_len * m_width + m_len * m_height + m_width * m_height); }
float Cuboid::volume() { return m_len * m_width * m_height; }//正方体
class Cube : public Cuboid {
public:Cube(float len);float area();float volume();
};
Cube::Cube(float len) : Cuboid(len, len, len) { }
float Cube::area() { return 6 * m_len * m_len; }
float Cube::volume() { return m_len * m_len * m_len; }int main() {//Line* p0 = new Rec(10, 20); // errorLine* p = new Cuboid(10, 20, 30);cout << "The area of Cuboid is " << p->area() << endl;cout << "The volume of Cuboid is " << p->volume() << endl;p = new Cube(15);cout << "The area of Cube is " << p->area() << endl;cout << "The volume of Cube is " << p->volume() << endl;return 0;
}
运行结果:
The area of Cuboid is 2200
The volume of Cuboid is 6000
The area of Cube is 1350
The volume of Cube is 3375
本例中定义了四个类,它们的继承关系为:Line --> Rec --> Cuboid --> Cube。
Line 是一个抽象类,也是最顶层的基类,在 Line 类中定义了两个纯虚函数 area() 和 volume()。
在 Rec 类中,实现了 area() 函数;所谓实现,就是定义了纯虚函数的函数体。但这时 Rec 仍不能被实例化,因为它没有实现继承来的 volume() 函数,volume() 仍然是纯虚函数,所以 Rec 也仍然是抽象类。
直到 Cuboid 类,才实现了 volume() 函数,才是一个完整的类,才可以被实例化。
可以发现,Line 类表示“线”,没有面积和体积,但它仍然定义了 area() 和 volume() 两个纯虚函数。这样的用意很明显:Line 类不需要被实例化,但是它为派生类提供了“约束条件”,派生类必须要实现这两个函数,完成计算面积和体积的功能,否则就不能实例化。
在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。
抽象基类除了约束派生类的功能,还可以实现多态。请注意第 51 行代码,指针 p 的类型是 Line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数;如果不这样做,51 行后面的代码都是错误的。我想,这或许才是C++提供纯虚函数的主要目的。
关于纯虚函数的几点说明
1) 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
2) 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。如下例所示:
//顶层函数不能被声明为纯虚函数
void fun() = 0; //compile error
class base{
public ://普通成员函数不能被声明为纯虚函数void display() = 0; //compile error
};
1.12.4. 虚析构函数
#include<iostream>
using namespace std;class ClxBase
{
public:ClxBase() {};virtual ~ClxBase() { cout << "delete ClxBase" << endl; };virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };
};class ClxDerived : public ClxBase
{
public:ClxDerived() {};~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };
};int main(int argc, char const* argv[])
{ClxBase* pTest = new ClxDerived;pTest->DoSomething();delete pTest;return 0;
}
打印结果如下:
Do something in class ClxDerived!
Output from the destructor of class ClxDerived!
delete ClxBase
如果基类ClxBase的析构函数没有定义成虚函数,那么打印结果为:
Do something in class ClxDerived!
delete ClxBase
即不会调用派生类的析构函数,这样会造成数据泄露的问题。
虚析构函数的作用:
(1)如果父类的析构函数不加virtual关键字
当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调用父类的析构函数,而不调用子类的析构函数。
(2)如果父类的析构函数加virtual关键字
当父类的析构函数声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,先调用子类的析构函数,再调用父类的析构函数。
1.12.5. 运行阶段类型识别 dynamic_cast
语法格式:派生类指针 = dynamic_cast<派生类类型 *>(基类指针);
如果转换成功,dynamic_cast返回对象的地址,如果失败,返回nullptr。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。class Hero // 英雄基类
{
public:int viability; // 生存能力。int attack; // 攻击伤害。virtual void skill1() { cout << "英雄释放了一技能。\n"; }virtual void skill2() { cout << "英雄释放了二技能。\n"; }virtual void uskill() { cout << "英雄释放了大绝招。\n"; }
};class XS :public Hero // 西施派生类
{
public:void skill1() { cout << "西施释放了一技能。\n"; }void skill2() { cout << "西施释放了二技能。\n"; }void uskill() { cout << "西施释放了大招。\n"; }void show() { cout << "我是天下第一美女。\n"; }
};class HX :public Hero // 韩信派生类
{
public:void skill1() { cout << "韩信释放了一技能。\n"; }void skill2() { cout << "韩信释放了二技能。\n"; }void uskill() { cout << "韩信释放了大招。\n"; }
};class LB :public Hero // 李白派生类
{
public:void skill1() { cout << "李白释放了一技能。\n"; }void skill2() { cout << "李白释放了二技能。\n"; }void uskill() { cout << "李白释放了大招。\n"; }
};int main()
{// 根据用户选择的英雄,施展一技能、二技能和大招。int id = 0; // 英雄的id。cout << "请输入英雄(1-西施;2-韩信;3-李白。):";cin >> id;// 创建基类指针,让它指向派生类对象,用基类指针调用派生类的成员函数。Hero* ptr = nullptr;if (id == 1) { // 1-西施ptr = new XS;}else if (id == 2) { // 2-韩信ptr = new HX;}else if (id == 3) { // 3-李白ptr = new LB;}if (ptr != nullptr) {ptr->skill1();ptr->skill2();ptr->uskill();// 如果基类指针指向的对象是西施,那么就调用西施的show()函数。//if (id == 1) {// XS* pxs = (XS *)ptr; // C风格强制转换的方法,程序员必须保证目标类型正确。// pxs->show();//}XS* xsptr = dynamic_cast<XS*>(ptr); // 把基类指针转换为派生类。if (xsptr != nullptr) xsptr->show(); // 如果转换成功,调用派生类西施的非虚函数。delete ptr;}// 以下代码演示把基类引用转换为派生类引用时发生异常的情况。/*HX hx;Hero& rh = hx;try{XS & rxs= dynamic_cast<XS &>(rh);}catch (bad_cast) {cout << "出现了bad_cast异常。\n";}*/
}
注意:
1)dynamic_cast只适用于包含虚函数的类。
2)dynamic_cast可以将派生类指针转换为基类指针,这种画蛇添足的做法没有意义。
3)dynamic_cast可以用于引用,但是,没有与空指针对应的引用值,如果转换请求不正确,会出现bad_cast异常。
1.13. 函数模板
template <typename T>
void Swap(T &a, T &b)
{T tmp = a;a = b;b = tmp;
}
1.13.1. 函数模板的注意事项
- 可以为类的成员函数创建模板,但不能是虚函数和析构函数。
#include<iostream>
#include<string>
using namespace std;class CGirl
{
public:template<typename T>CGirl(T a){cout << "a= " << a << endl;}template<typename T>void show(){cout << "show方法" << endl;}// 错误的//template<typename T>//virtual void show()//{// cout << "show方法" << endl;//}//template<typename T>//~CGirl()//{//}
};int main()
{int a = 10;CGirl g = CGirl(a);g.show<int>();
}
- 使用函数模板时,必须明确数据类型,确保实参与函数模板能匹配上。
#include<iostream>
#include<string>
using namespace std;template <typename T>
void Swap(T &a, T &b) // 传引用
{T tmp = a;a = b;b = tmp;
}int main()
{// 错误的,传引用,必须是数据类型一致的,不能进行隐式转换//int a = 10;//char b = 30;//Swap(a, b);
}
#include<iostream>
#include<string>
using namespace std;template <typename T>
void Swap(T a, T b) // 传值
{T tmp = a;a = b;b = tmp;
}int main()
{// 正确的int a = 10;char b = 30;Swap<int>(a, b); // 可以发生隐式转换
}
#include<iostream>
#include<string>
using namespace std;// 函数模板中没有用到模板参数
template <typename T>
void Swap()
{cout << "调用了Swap函数" << endl;
}int main()
{// 错误的//Swap();// 正确的,显式的指定。Swap<int>();
}
- 使用函数模板时,推导的数据类型必须适应函数模板中的代码。
#include<iostream>
#include<string>
using namespace std;template <typename T>
T Add(T a, T b)
{return a + b;
}class CGirl
{
};
int main()
{//错误的,CGirl对象没有+运算//CGirl g1;//CGirl g2;//Add(g1 + g2);
}
- 使用函数模板时,如果是自动类型推导,不会发生隐式类型转换,如果显式指定了函数模板的数据类型,可以发生隐式类型转换。
#include<iostream>
#include<string>
using namespace std;template <typename T>
void Swap(T a, T b)
{T tmp = a;a = b;b = tmp;
}int main()
{// 正确的int a = 10;char b = 30;Swap<int>(a, b); // 显式指定了int数据类型,可以发生从char到int的数据类型转换
}
1.13.2. 函数模板具体化
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。class CGirl // 超女类。
{
public:int m_bh; // 编号。string m_name; // 姓名。int m_rank; // 排名。
};template <typename T>
void Swap(T& a, T& b); // 交换两个变量的值函数模板。template<>
void Swap<CGirl>(CGirl& g1, CGirl& g2); // 交换两个超女对象的排名。这个函数是函数模板的具体化函数
// template<>
// void Swap(CGirl& g1, CGirl& g2); // 交换两个超女对象的排名。int main()
{int a = 10, b = 20;Swap(a, b); // 使用了函数模板。cout << "a=" << a << ",b=" << b << endl;CGirl g1, g2;g1.m_rank = 1; g2.m_rank = 2;Swap(g1, g2); // 使用了超女类的具体化函数。cout << "g1.m_rank=" << g1.m_rank << ",g2.m_rank=" << g2.m_rank << endl;
}
编译器使用各种函数的规则:
- 具体化优先于常规模板,普通函数优先于具体化和常规模板。
- 如果希望使用函数模板,可以用空模板参数强制使用函数模板。
- 如果函数模板能产生更好的匹配,将优先于普通函数。
#include <iostream> // 包含头文件。
#include<string>
using namespace std; // 指定缺省的命名空间。void Swap(int a, int b) // 普通函数。
{cout << "使用了普通函数。\n";
}template <typename T>
void Swap(T a, T b) // 函数模板。
{cout << "使用了函数模板。\n";
}template <>
void Swap(int a, int b) // 函数模板的具体化版本。
{cout << "使用了具体化的函数模板。\n";
}int main()
{Swap(1,2); // 会调用普通函数Swap('c', 'd'); // 会调用函数模板,因为不用进行隐式转换Swap<>(1,2); // 用空模板,会强制调用函数模板的具体化版本
}
1.13.3. 函数模板分文件编写
记住下面两点就可以了:
- 函数模板只是函数的描述,没有实体,创建函数模板的代码放在头文件中。
- 函数模板的具体化有实体,编译的原理和普通函数一样,所以,声明放在头文件中,定义放在源文件中。
1.13.4. 函数模板高级
1.13.4.1. decltype关键字
语法:decltype(expression) var;
作用:用于分析表达式的数据类型
#include <iostream> // 包含头文件。
#include<string>
using namespace std; // 指定缺省的命名空间。template <typename T>
auto Swap(T a, T b) // 函数模板。
{decltype(a + b) temp = a + b;cout << "temp = " << temp << endl;return temp;
}int main()
{auto res = Swap('c', 'd');cout << res << endl;
}
#include <iostream> // 包含头文件。
#include<string>
using namespace std; // 指定缺省的命名空间。int func()
{cout << "调用了func函数" << endl;return 3;
}int main()
{decltype(func()) f = func(); // 函数返回值的数据类型cout << f << endl;decltype(func) *f = func; // 函数类型f(); // 调用func函数
}
1.13.4.2. typename 的用法
template <typename T>
struct MakeUniqueResult {using scalar = std::unique_ptr<T>;
};template <typename T, typename... Args>
typename MakeUniqueResult<T>::scalar make_unique(Args &&... args) { // NOLINTreturn std::unique_ptr<T>(new T(std::forward<Args>(args)...)); // NOLINT(build/c++11)
}
上面代码中,编译器无法自动区分 MakeUniqueResult<T>::scalar 是一个类型还是一个成员变量。为了明确告诉编译器 scalar 是一个类型,我们使用 typename 关键字。没有 typename,编译器会产生错误,因为它不能确定 scalar 的含义。
1.14. 类模板
1.14.1. 语法
template <class T>
class 类模板名
{类的定义;
};
1.14.2. 注意事项
- 在创建对象的时候,必须指明具体的数据类型。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。template <class T1, class T2>
class AA
{
public:T1 m_a; // 通用类型用于成员变量。T2 m_b; // 通用类型用于成员变量。AA() { } // 默认构造函数是空的。// 通用类型用于成员函数的参数。AA(T1 a, T2 b) :m_a(a), m_b(b) { }// 通用类型用于成员函数的返回值。T1 geta() // 获取成员m_a的值。{T1 a = 2; // 通用类型用于成员函数的代码中。return m_a + a;}T2 getb(); // 获取成员m_b的值。
};// 模板类的成员函数可以在类外实现。
template<class T1, class T2>
T2 AA<T1, T2>::getb()
{return m_b;
}
int main()
{AA<int, string>* a; // 在创建对象的时候,必须指明具体的数据类型。AA a 是错误的。
}
- 使用类模板时,数据类型必须适应类模板中的代码。
- 类模板可以为通用数据类型指定缺省的数据类型。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。template <class T1, class T2=string> // 指定通用数据类型的数据类型
class AA
{
}
- 模板类的成员函数可以在类外实现。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。template <class T1, class T2>
class AA
{
public:T1 m_a; // 通用类型用于成员变量。T2 m_b; // 通用类型用于成员变量。AA() { } // 默认构造函数是空的。// 通用类型用于成员函数的参数。AA(T1 a, T2 b) :m_a(a), m_b(b) { }T2 getb(); // 获取成员m_b的值。
};// 模板类的成员函数可以在类外实现。
template<class T1, class T2>
T2 AA<T1, T2>::getb()
{return m_b;
}
- 可以用new创建模板类对象。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。template <class T1, class T2=string>
class AA
{
public:T1 m_a; // 通用类型用于成员变量。T2 m_b; // 通用类型用于成员变量。AA() { } // 默认构造函数是空的。// 通用类型用于成员函数的参数。AA(T1 a, T2 b) :m_a(a), m_b(b) { }// 通用类型用于成员函数的返回值。T1 geta() // 获取成员m_a的值。{T1 a = 2; // 通用类型用于成员函数的代码中。return m_a + a;}
};int main()
{AA<int, string> *b = new AA<int, string>();
}
- 在程序中,模板类的成员函数使用了才会创建。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。template <class T1, class T2=string>
class AA
{
public:T1 m_a; // 通用类型用于成员变量。T2 m_b; // 通用类型用于成员变量。AA() { } // 默认构造函数是空的。// 该成员函数并不会被调用,因此也不会报错T1 gethaha(){return m_a.hahaha();}// 通用类型用于成员函数的参数。AA(T1 a, T2 b) :m_a(a), m_b(b) { }
};int main()
{AA<int, string>* a; //在程序中,模板类的成员函数使用了才会创建。
}
1.14.3. 类模板的具体化(重点)
- 可以部分具体化,也可以完全具体化;
- 具体化程度高的类优先于具体化程度低的类,具体化的类优先于没有具体化的类。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。/
// 类模板
template<class T1, class T2>
class AA { // 类模板。
public:T1 m_x;T2 m_y;AA(const T1 x, const T2 y) :m_x(x), m_y(y) { cout << "类模板:构造函数。\n"; }void show() const;
};template<class T1, class T2>
void AA<T1, T2>::show() const { // 成员函数类外实现。cout << "类模板:x = " << m_x << ", y = " << m_y << endl;
}
/
// 类模板完全具体化
template<>
class AA<int, string> {
public:int m_x;string m_y;AA(const int x, const string y) :m_x(x), m_y(y) { cout << "完全具体化:构造函数。\n"; }void show() const;
};void AA<int, string>::show() const { // 成员函数类外实现。cout << "完全具体化:x = " << m_x << ", y = " << m_y << endl;
}
/
// 类模板部分具体化
template<class T1>
class AA<T1, string> {
public:T1 m_x;string m_y;AA(const T1 x, const string y) :m_x(x), m_y(y) { cout << "部分具体化:构造函数。\n"; }void show() const;
};template<class T1>
void AA<T1, string>::show() const { // 成员函数类外实现。cout << "部分具体化:x = " << m_x << ", y = " << m_y << endl;
}
/int main()
{// 具体化程度高的类优先于具体化程度低的类,具体化的类优先于没有具体化的类。AA<int, string> aa1(8, "我是一只傻傻鸟。"); // 将使用完全具体化的类。AA<char, string> aa2(8, "我是一只傻傻鸟。"); // 将使用部分具体化的类。AA<int, double> aa3(8, 999999); // 将使用模板类。
}
1.14.4. 类模板与继承
模板类继承普通类
#include<string>
#include<iostream>using namespace std;// 普通类
class AA
{
public:int m_a;AA(int a) :m_a(a) { cout << "调用了AA的构造函数。\n"; }void func1() { cout << "调用了func1()函数:m_a=" << m_a << endl;; }
};// 模板类
template<class T1, class T2>
class BB:public AA
{
public:T1 m_x;T2 m_y;BB(const T1 x, const T2 y, int a) :AA(a), m_x(x), m_y(y){cout << "调用了BB的构造函数。\n";}// 常函数void func2() const{cout << "调用了func2()函数:x = " << m_x << ", y = " << m_y << endl;}
};int main()
{BB<int, string> bb(8, "我是一只傻傻鸟", 3);bb.func2();}
普通类继承模板类的实例化版本
#include<string>
#include<iostream>using namespace std;// 模板类
template<class T1, class T2>
class AA
{
public:T1 m_x;T2 m_y;// 构造函数AA(const T1 x, const T2 y) : m_x(x), m_y(y) { cout << "调用了AA的构造函数。\n"; }// 常函数void func1() const{cout << "调用了func1()函数:x = " << m_x << ", y = " << m_y << endl;}
};// 普通类
class BB:public AA<int,string>
{
public:int m_a;BB(int a, int x, string y) : AA(x, y), m_a(a) { cout << "调用了BB的构造函数。\n"; }void func2() { cout << "调用了func2()函数:m_a = " << m_a << endl;; }
};int main()
{BB bb(3, 8, "我是一只傻傻鸟。");bb.func1();bb.func2();
}
// 28行代码 AA(x, y)
普通类继承模板类
#include<string>
#include<iostream>using namespace std;// 模板类
template<class T1, class T2>
class AA
{
public:T1 m_x;T2 m_y;// 构造函数AA(const T1 x, const T2 y) : m_x(x), m_y(y) { cout << "调用了AA的构造函数。\n"; }// 常函数void func1() const{cout << "调用了func1()函数:x = " << m_x << ", y = " << m_y << endl;}
};// 普通类
template<class T1, class T2>
class BB :public AA<T1, T2>
{
public:int m_a;BB(int a, const T1 x, const T2 y) : AA<T1, T2>(x, y), m_a(a) { cout << "调用了BB的构造函数。\n"; }void func2() { cout << "调用了func2()函数:m_a = " << m_a << endl;; }
};int main()
{BB<int,string> bb(3, 8, "我是一只傻傻鸟。");bb.func1();bb.func2();
}// 关键代码在29行, AA<T1, T2>(x, y)需要指定泛型
模板类继承模板类
#include<string>
#include<iostream>using namespace std;template<class T1, class T2>
class BB
{
public:T1 m_x;T2 m_y;BB(const T1 x, const T2 y) : m_x(x), m_y(y) { cout << "调用了BB的构造函数。\n"; }void func2() const { cout << "调用了func2()函数:x = " << m_x << ", y = " << m_y << endl; }
};template<class T, class T1, class T2>
class CC :public BB<T1, T2> // 模板类继承模板类。
{
public:T m_a;CC(const T a, const T1 x, const T2 y) : BB<T1, T2>(x, y), m_a(a) { cout << "调用了CC的构造函数。\n"; }void func3() { cout << "调用了func3()函数:m_a = " << m_a << endl;; }
};int main()
{CC<int, int, string> cc(3, 8, "我是一只傻傻鸟。");cc.func3();cc.func2();
}
模板类继承模板参数给出的基类
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。class AA {
public:AA() { cout << "调用了AA的构造函数AA()。\n"; }AA(int a) { cout << "调用了AA的构造函数AA(int a)。\n"; }
};class BB {
public:BB() { cout << "调用了BB的构造函数BB()。\n"; }BB(int a) { cout << "调用了BB的构造函数BB(int a)。\n"; }
};class CC {
public:CC() { cout << "调用了CC的构造函数CC()。\n"; }CC(int a) { cout << "调用了CC的构造函数CC(int a)。\n"; }
};template<class T>
class DD {
public:DD() { cout << "调用了DD的构造函数DD()。\n"; }DD(int a) { cout << "调用了DD的构造函数DD(int a)。\n"; }
};template<class T>
class EE : public T { // 模板类继承模板参数给出的基类。
public:EE() :T() { cout << "调用了EE的构造函数EE()。\n"; }EE(int a) :T(a) { cout << "调用了EE的构造函数EE(int a)。\n"; }
};int main()
{EE<AA> ea1; // AA作为基类。EE<BB> eb1; // BB作为基类。EE<CC> ec1; // CC作为基类。EE<DD<int>> ed1; // EE<int>作为基类。// EE<DD> ed1; // DD作为基类,错误。
}
1.14.5. 类模板与函数
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。template<class T1, class T2>
class AA // 模板类AA。
{
public:T1 m_x;T2 m_y;AA(const T1 x, const T2 y) : m_x(x), m_y(y) { }void show() const { cout << "show() x = " << m_x << ", y = " << m_y << endl; }
};// 采用普通函数,参数和返回值是模板类AA的实例化版本。
AA<int, string> func(AA<int, string>& aa)
{aa.show();cout << "调用了func(AA<int, string> &aa)函数。\n";return aa;
}// 函数模板,参数和返回值是的模板类AA。
template <typename T1, typename T2>
AA<T1, T2> func(AA<T1, T2>& aa)
{aa.show();cout << "调用了func(AA<T1, T2> &aa)函数。\n";return aa;
}// 函数模板,参数和返回值是任意类型。
template <typename T>
T func(T& aa)
{aa.show();cout << "调用了func(AA<T> &aa)函数。\n";return aa;
}int main()
{AA<int, string> aa(3, "我是一只傻傻鸟。");func(aa);
}
1.14.6. 类模板与友元
非模板友元
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。template<class T1, class T2>
class AA
{T1 m_x;T2 m_y;
public:AA(const T1 x, const T2 y) : m_x(x), m_y(y) { }// 非模板友元:友元函数不是模板函数,而是利用模板类参数生成的函数,只能在类内实现。friend void show(const AA<T1, T2>& a){cout << "x = " << a.m_x << ", y = " << a.m_y << endl;}/* friend void show(const AA<int, string>& a);friend void show(const AA<char, string>& a);*/
};//void show(const AA<int, string>& a)
//{
// cout << "x = " << a.m_x << ", y = " << a.m_y << endl;
//}
//
//void show(const AA<char, string>& a)
//{
// cout << "x = " << a.m_x << ", y = " << a.m_y << endl;
//}int main()
{AA<int, string> a(88, "我是一只傻傻鸟。");show(a);AA<char, string> b(88, "我是一只傻傻鸟。");show(b);
}
约束模板友元
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。// 约束模板友元:模板类实例化时,每个实例化的类对应一个友元函数。
template <typename T>
void show(T& a); // 第一步:在模板类的定义前面,声明友元函数模板。template<class T1, class T2>
class AA // 模板类AA。
{friend void show<>(AA<T1, T2>& a); // 第二步:在模板类中,再次声明友元函数模板。T1 m_x;T2 m_y;public:AA(const T1 x, const T2 y) : m_x(x), m_y(y) { }
};template<class T1, class T2>
class BB // 模板类BB。
{friend void show<>(BB<T1, T2>& a); // 第二步:在模板类中,再次声明友元函数模板。T1 m_x;T2 m_y;public:BB(const T1 x, const T2 y) : m_x(x), m_y(y) { }
};template <typename T> // 第三步:友元函数模板的定义。
void show(T& a)
{cout << "通用:x = " << a.m_x << ", y = " << a.m_y << endl;
}template <> // 第三步:具体化版本。
void show(AA<int, string>& a)
{cout << "具体AA<int, string>:x = " << a.m_x << ", y = " << a.m_y << endl;
}template <> // 第三步:具体化版本。
void show(BB<int, string>& a)
{cout << "具体BB<int, string>:x = " << a.m_x << ", y = " << a.m_y << endl;
}int main()
{AA<int, string> a1(88, "我是一只傻傻鸟。");show(a1); // 将使用具体化的版本。AA<char, string> a2(88, "我是一只傻傻鸟。");show(a2); // 将使用通用的版本。BB<int, string> b1(88, "我是一只傻傻鸟。");show(b1); // 将使用具体化的版本。BB<char, string> b2(88, "我是一只傻傻鸟。");show(b2); // 将使用通用的版本。
}
非约束模板友元
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。// 非类模板约束的友元函数,实例化后,每个函数都是每个每个类的友元。
template<class T1, class T2>
class AA
{template <typename T> friend void show(T& a); // 把函数模板设置为友元。T1 m_x;T2 m_y;
public:AA(const T1 x, const T2 y) : m_x(x), m_y(y) { }
};template <typename T> void show(T& a) // 通用的函数模板。
{cout << "通用:x = " << a.m_x << ", y = " << a.m_y << endl;
}template <>void show(AA<int, string>& a) // 函数模板的具体版本。
{cout << "具体<int, string>:x = " << a.m_x << ", y = " << a.m_y << endl;
}int main()
{AA<int, string> a(88, "我是一只傻傻鸟。");show(a); // 将使用具体化的版本。AA<char, string> b(88, "我是一只傻傻鸟。");show(b); // 将使用通用的版本。
}
1.14.7. 成员类模板
#include<iostream>using namespace std;template<class T1, class T2>
class AA
{
public:T1 m_x;T2 m_y;AA(const T1 x, const T2 y):m_x(x), m_y(y){cout << "调用AA的构造函数\n";}void show(){cout << "m_x=" << m_x << ",m_y=" << m_y << endl;}template<class T>class BB{public:T m_a;T1 m_b;BB() {};void show();};BB<string> m_bb;template<typename T>void show(T tt);
};// 顺序不能写反
template<class T1, class T2>
template<class T>
void AA<T1, T2>::BB<T>::show()
{cout << "m_a=" << m_a << ",m_b=" << m_b << endl;
}// 顺序不能写反
template<class T1, class T2>
template<typename T>
void AA<T1, T2>::show(T t)
{cout << "tt=" << t << endl;cout << "m_x=" << m_x << ",m_y=" << m_y << endl;m_bb.show();
}
int main()
{AA<int, string> a(88, "我是一只傻傻鸟。");a.show();a.m_bb.m_a = "我有一只小小鸟。";a.m_bb.show();a.show("你是一只什么鸟?");
}
1.14.8. 类模板做参数
#include<iostream>using namespace std;template<class T, int len>
class LinkedList
{
public:T* m_head; // 链表头节点int m_len = len; // 链表长度void insert() { cout << "向链表中插入了一条记录。\n"; }void m_delete() { cout << "向链表中删除了一条记录。\n"; }void update() { cout << "向链表中更新了一条记录。\n"; }
};template <class T, int len>
class Array // 数组类模板
{
public:T* m_data; // 数组指针int m_len = len; // 数组长度void insert() { cout << "向数组中插入了一条记录。\n"; }void m_delete() { cout << "向数组中删除了一条记录。\n"; }void update() { cout << "向数组中更新了一条记录。\n"; }
};//核心代码 template<class , int> class 表示模板类参数
template<template<class , int> class table_type, class data_type, int len>
class LinearList
{
public:table_type<data_type, len> m_table;void insert() { m_table.insert(); } // 线性表插入操作。void m_delete() { m_table.m_delete(); } // 线性表删除操作。void update() { m_table.update(); } // 线性表更新操作。void oper() // 按业务要求操作线性表。{cout << "len=" << m_table.m_len << endl;m_table.insert();m_table.update();}
};
int main()
{// 创建线性表对象,容器类型为链表,链表的数据类型为int,表长为20。LinearList<LinkedList, int, 20> a;a.insert();a.m_delete();a.update();// 创建线性表对象,容器类型为数组,数组的数据类型为string,表长为20。LinearList<Array, string, 20> b;b.insert();b.m_delete();b.update();}
2. 强制转换
2.1. static_cast
用于内置数据类型之间的转换
#include <iostream>
using namespace std;int main(int argc, char* argv[])
{int ii = 3;long ll = ii; // 绝对安全,可以隐式转换,不会出现警告。double dd = 1.23;long ll1 = dd; // 可以隐式转换,但是,会出现可能丢失数据的警告。long ll2 = (long)dd; // C风格:显式转换,不会出现警告。long ll3 = static_cast<long>(dd); // C++风格:显式转换,不会出现警告。cout << "ll1=" << ll1 << ",ll2=" << ll2 << ",ll3=" << ll3 << endl;
}
用于指针之间的转换
#include <iostream>
using namespace std;void func(void* ptr) { // 其它类型指针 -> void *指针 -> 其它类型指针double* pp = static_cast<double*>(ptr);
}int main(int argc, char* argv[])
{int ii = 10;//double* pd1 = ⅈ // 错误,不能隐式转换。double* pd2 = (double*) ⅈ // C风格,强制转换。//double* pd3 = static_cast<double*>(&ii); // 错误,static_cast不支持不同类型指针的转换。void* pv = ⅈ // 任何类型的指针都可以隐式转换成void*。double* pd4 = static_cast<double*>(pv); // static_cast可以把void *转换成其它类型的指针。func(&ii);
}
2.2. const_cast
2.3. reinterpret_cast
类似c风格的强制转换
int* p = nullptr;
double* b = reinterpret_cast<double*>(p); //正确,但是有风险
2.4. dynamic_cast
主要用在继承结构中,可以支持RTTI类型识别的上下转换
相关文章:

c++(二)
1. 类和对象 1.1. 封装 封装的意义 将属性和行为作为一个整体,表现生活中的事物;将属性和行为加以权限控制 public -> 公共权限:类内可以访问,类外也可以访问protected -> 保护权限:类内可以访问,…...

基于PHP的初中数学题库管理系统
有需要请加文章底部Q哦 可远程调试 基于PHP的初中数学题库管理系统 一 介绍 此初中数学题库管理系统基于原生PHP开发,数据库mysql,系统角色分为学生,教师和管理员。(附带参考设计文档) 技术栈:phpmysqlphpstudyvscode 二 功能 …...

WDG看门狗
1 WDG 1.1 简介 WDG是看门狗定时器(Watchdog Timer)的缩写,它是一种用于计算机和嵌入式系统中的定时器,用来检测和恢复系统故障。 看门狗就像是一个忠诚的宠物狗,它时刻盯着你的程序,确保它们正常运行。…...

zabbix server client 安装配置
Zabbix Server 采用源码包部署,数据库采用 MySQL8.0 版本,zabbix-web 使用 nginxphp 来实现。具体信息如下: 软件名 版本 安装方式 Zabbix Server 6.0.3 源码安装 Zabbix Agent 6.0.3 源码安装 MySQL 8.0.28 yum安装 Nginx 1.20…...

Unity关于Addressables.Release释放资源内存问题
前言 最近在编写基于Addressables的资源管理器,对于资源释放模块配合MemoryProfiler进行了测试,下面总结下测试Addressables.Release的结论。 总结 使用Addressables.Release释放资源时,通过MemoryProfiler检查内存信息发现加载的内容还在…...

运算放大器(运放)带宽和带宽平坦度
运算放大器带宽和带宽平坦度 电压反馈型运算放大器的带宽 下图1显示电压反馈型运算放大器的开环频率响应。有两种可能:图1A是最常见的情况,高直流增益以6dB/倍频程从极低频率下降至单位增益,也就是典型的单极点响应。相比之下,图…...
npm常用命令使用与事件案例
概述 npm(Node Package Manager)是一个JavaScript编程语言的包管理器,用于Node.js应用程序。它允许用户安装、共享和管理具有重复使用价值的代码(包),这些代码可以是库、工具或应用程序。 npm常用命令详解…...
Spring Boot中的定时任务调度
Spring Boot中的定时任务调度 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将深入探讨如何在Spring Boot应用中实现定时任务调度,这在实际…...

Hadoop3:MapReduce中的ETL(数据清洗)
一、概念说明 “ETL,是英文Extract-Transform-Load的缩写,用来描述将数据从来源端经过抽取(Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL一词较常用在数据仓库&#…...

python解锁图片相似度的神奇力量
在这个信息爆炸的时代,图片成为了我们传递信息、表达情感和记录生活的重要方式。然而,面对海量的图片资源,如何快速准确地找到相似的图片,成为了一个亟待解决的问题。现在,让我们为您揭开图片相似度的神秘面纱,带您领略这一创新技术的魅力! 图片相似度技术,就像是一位…...
TensorFlow 的原理与使用
文章目录 TensorFlow 的基本原理1. 计算图(Computation Graph)2. 张量(Tensor)3. 会话(Session)4. 自动微分(Automatic Differentiation) TensorFlow 的使用安装 TensorFlow基本使用…...

[数据库]事务的隔离级别存储引擎
事务的隔离级别 存储引擎 举例 myisam 进行回滚操作后可以发现有一个警告没有行受到影响 memory 比如用于qq的在线离线状态...

使用nvm切换node版本时报错:exit status 1解决办法
作者介绍:计算机专业研究生,现企业打工人,从事Java全栈开发 主要内容:技术学习笔记、Java实战项目、项目问题解决记录、AI、简历模板、简历指导、技术交流、论文交流(SCI论文两篇) 上点关注下点赞 生活越过…...
Kafka~高吞吐量设计
Kafka 之所以能够实现高性能和高速度,主要归因于以下几个关键因素: 分布式架构:Kafka 采用分布式架构,可以水平扩展,通过增加服务器节点来处理更多的流量和数据存储。顺序写入磁盘:Kafka 将消息顺序地写入…...

STM32小项目———感应垃圾桶
文章目录 前言一、超声波测距1.超声波简介2.超声波测距原理2.超声波测距步骤 二、舵机的控制三、硬件搭建及功能展示总结 前言 一个学习STM32的小白~ 有问题请评论区或私信指出 提示:以下是本篇文章正文内容,下面案例可供参考 一、超声波测距 1.超声波…...
嵌入式MCU平台汇总
文章目录 1. 单片机(MCU) 2. 数字信号处理器(DSP) 3. ARM Cortex 系列 4. 超低功耗MCU 5. 物联网MCU(IoT MCU) 6. 开源架构MCU(RISC-V) 7. 可编程逻辑器件(FPGA&a…...

C#udpClient组播
一、0udpClient 控件: button(打开,关闭,发送),textbox,richTextBox 打开UDP: UdpClient udp: namespace _01udpClient {public partial class Form1 : Form{public Form1(){Initi…...

《昇思25天学习打卡营第14天 | 昇思MindSpore基于MindNLP+MusicGen生成自己的个性化音乐》
14天 本节学了基于MindNLPMusicGen生成自己的个性化音乐。 MusicGen是来自Meta AI的Jade Copet等人提出的基于单个语言模型的音乐生成模型,能够根据文本描述或音频提示生成高质量的音乐样本。 MusicGen模型基于Transformer结构,可以分解为三个不同的阶段…...

新奥集团校招面试经验分享、测评笔试题型分析
一、走进新奥集团 新奥集团成立于1989年,总部位于河北廊坊,是中国领先的清洁能源企业集团。业务涵盖城市燃气、能源化工、环保科技等多个领域,致力于构建现代能源体系,提升生活品质。 二、新奥集团校招面试经验分享 新奥集团的…...

【推荐】Prometheus+Grafana企业级监控预警实战
新鲜出炉!!!PrometheusGrafanaAlertmanager springboot 企业级监控预警实战课程,从0到1快速搭建企业监控预警平台,实现接口调用量统计,接口请求耗时统计…… 详情请戳 https://edu.csdn.net/course/detai…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...

Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...

微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列,以便知晓哪些列包含有价值的数据,…...
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的“no matching...“系列算法协商失败问题
【SSH疑难排查】轻松解决新版OpenSSH连接旧服务器的"no matching..."系列算法协商失败问题 摘要: 近期,在使用较新版本的OpenSSH客户端连接老旧SSH服务器时,会遇到 "no matching key exchange method found", "n…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...

LabVIEW双光子成像系统技术
双光子成像技术的核心特性 双光子成像通过双低能量光子协同激发机制,展现出显著的技术优势: 深层组织穿透能力:适用于活体组织深度成像 高分辨率观测性能:满足微观结构的精细研究需求 低光毒性特点:减少对样本的损伤…...

android RelativeLayout布局
<?xml version"1.0" encoding"utf-8"?> <RelativeLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:gravity&…...

C++中vector类型的介绍和使用
文章目录 一、vector 类型的简介1.1 基本介绍1.2 常见用法示例1.3 常见成员函数简表 二、vector 数据的插入2.1 push_back() —— 在尾部插入一个元素2.2 emplace_back() —— 在尾部“就地”构造对象2.3 insert() —— 在任意位置插入一个或多个元素2.4 emplace() —— 在任意…...