自动类型转换和函数模板
自动类型转换函数
首先我们先来分析内置类型,如果两种数据类型是兼容的,在C++ 中可以自动转换,如果要从更大的数转换为更小的数,就可能会发生截断。
long count = 8;//将int 转换为long
double time= 11;//将int转换为 double
int size = 3.33 //将double转换为int的3
从底层角度分析,不同数据类型的差别在于取值范围和精度,数据的取值范围越大,精度越高。
整型从低到高:
char -> short -> int -> long -> long long
浮点型从低到高:
float -> double -> long double
所以数据在进行自动类型转换时 遵循规则如下:
- 1、如果一个表达式中出现了不同类型操作数的混合运算,较低类型将自动向较高类型转换。
- 2、赋值运算的右值类型与左值类型不一致时,将右值类型提升/降低为左值类型。
- 3、赋值运算右值超出了左值类型的表示范围,把该右值截断后赋给左值,所得结果可能毫无意义。
- 4、当表达式中含有浮点型操作数时,所有操作数都将转换为浮点型。
这种转换工作,是编译器自动完成的,可以隐式的进行,不需要程序员干预。
对应需要程序员干预的,类型转换,也称为强制类型转换(显式转换)。比如下面这个例子。
double rel = (double)10/2; //本来10/2 = 5是int类型 表示程序员指定要double类型的结果,编译报错后果我承担。编译器会把5=>5.0再赋值给rel
现在我们再来看自定义数据类型,也就是类。类也会发生自定类型转换,那么他的转换规则,根据书籍描述如下:
在C++中,将一个参数的构造函数用作自动类型转换函数,它是自动进行的,不需要显式的转换。
Person p(123); //正统的C++写法,创建一个对象 用() 表明正在调用构造函数对对象属性进行初始化赋值
Person p = Person(100); //显示转换,系统会先调用默认构造函数创建Person对象p,再把int参数构造函数后分配对象引用赋值给p
Person p;
p = Person(100); //等价写法
因为存在类中定义了自动转换函数,所以也允许开发者这样写。
Person p = 123; //会发生隐式转换 调用自动类型转换函数 将int类型 转换Person类型
Person p;
p = 123; //同理,赋值运算符 会先将int类型 转换为Person类型后 再赋值给 p
示例:看看存在自动类型转换后,我们可以采用的构建对象策略
#include <iostream>
#include <vector>
#include <list>
using namespace std;
class Person {
friend ostream& operator<<(ostream& cout, const Person& p) {
return cout << "Person(id=" << p.id << ",name=" << p.name << ")";
}
private:
int id;
string name;
public:
Person(){}
Person(const int id) {
this->id = id;
}
Person(const string& name) {
this->name = name;
}
};
void printInfo(const Person& p) {
cout << p;
}
int main()
{
Person p = 12;
string name = "wangnaixing";
Person p = name; //看着就不像创建对象,对成员变量初始化。但他确实初始化了~~~
Person p;
p = name;
p = 12;
printInfo(12); //这调用,这方法定义你不看你能知道形参是个类??
printInfo(name);
}
总结一下:
1)一个类可以有多个转换函数。
2)多个参数的构造函数,除第一个参数外,如果其它参数都有缺省值,也可以作为转换函数。意味着我们可以通过自动类型转换,让编译器自己去找到指定的构造函数。
3)隐式转换发生的场景:
将对象Person初始化为int值时 Person p = 12;
将int值通过赋值运算符赋给Person对象时 Person p; p = 12;
将int值传递给接受Person参数的函数时 printInfo(12);
函数返回值被声明为Person 试图返回int值时
explicit
将构造函数用作自动类型转换函数似乎是一项不错的特性,但有时候会导致意外的类型转换。explicit关键字用于关闭这种自动特性,在实际开发中,如果强调的是构造,建议使用explicit,如果强调的是类型转换,则不使用explicit。
我们来演示下,在Person(const int id)
构造方法中,加入 explicit
关键字。
explicit Person(const int id) { //强调是构造不允许你用自动类型转换
this->id = id;
}
提示标错了。
转换函数
构造函数只能用于某个内置类型转换为类类型,如果我们想逆向这个过程,将类类型 转换为内置类型改如何处理呢?
如果要进行相反的转换,可以使用特殊的运算符函数-转换函数。
语法:operator 数据类型();
注意:转换函数必须是类的成员函数;不能指定返回值类型;不能有参数。
#include <iostream>
#include <vector>
#include <list>
using namespace std;
class Person {
friend ostream& operator<<(ostream& cout, const Person& p) {
return cout << "Person(id=" << p.id << ",name=" << p.name << ")";
}
private:
int id;
string name;
public:
Person(){}
explicit Person(const int id) { //强调是构造不允许你用自动类型转换
this->id = id;
}
Person(const string& name) {
this->name = name;
}
void setName(const string& name) {
this->name = name;
}
void setId(const int id) {
this->id = id;
}
//转换函数在这里
operator string() {
return name;
}
operator int() {
return id;
}
};
int main()
{
Person p;
p.setId(1);
p.setName("wangnaixing");
cout << (int)p; //输出1
cout << (string)p; //输出wangnaixing
string demo = p;
int demo2 = p;
cout << demo; //隐式调用 真的炸
cout << demo2; //如果真的有兄弟这么用,真的就自求多福了。
}
函数模板
函数模板是比方法重载,更简洁的实现代码复用的方式。这种情况下的重载,内部逻辑都是一样的,唯一的区别就是数据类型不同。
void swap(int& x1, int& x2) {
int temp = x1;
x1 = x2;
x2 = temp; //看看重载的逻辑,为了适配不同数据类型,却需要定义三个不同的函数。这种便利只能在调用时被体现。
}
void swap(double& x1, double& x2) {
double temp = x1;
x1 = x2;
x2 = temp;
}
void swap(long& x1, long& x2) {
long temp = x1;
x1 = x2;
x2 = temp;
}
所以,我们可以考虑使用函数模板来优化下。
#include <iostream>
using namespace std;
template <typename T>
void Swap(T& x1, T& x2); //超级像Java的泛型
int main()
{
int a = 3, b = 4;
Swap(a, b);
cout << a << b;
double x = 3.3,y = 4.4;
Swap(x, y);
cout << x << y;
}
template <typename T>
void Swap(T& x1, T& x2) { //是不是用了模板之后,比重载在代码层面上更加整洁了?
T temp = x1;
x1 = x2;
x2 = temp;
}
如果出现了函数模板的逻辑,不适用与某些类型的情况,比如我自己定义的Student
类,我期望是交换Student
类里面的成员age
这是时候我就可以定义函数模板的具体化
模板的具体化
具体化(特例化、特化)的语法:
template<> void 函数模板名<数据类型>(参数列表)
template<> void 函数模板名 (参数列表)
{
// 函数体。
}
#include <iostream>
using namespace std;
template <typename T>
void Swap(T& x1, T& x2);
class Student
{
friend ostream& operator <<(ostream& cout, const Student& stu) {
return cout << "Student(id=" << stu.id << ",name=" << stu.name << ",age=" << stu.age << ")";
}
private:
int id;
string name;
int age;
public:
Student(const int id, const string name, const int age) {
this->id = id;
this->name = name;
this->age = age;
}
int getAge() {
return age;
}
void setAge(const int age) {
this->age = age;
}
};
template<typename T>
void Swap(T& a, T& b);
template<> //Studnet 是Swap模板的一个需要特情处理的情况
void Swap<Student>(Student& s1, Student& s2);
int main()
{
Student s1{ 1,"wangnaixing",20 };
Student s2{ 2,"zhangsan",19 };
Swap(s1, s2);
cout << s1;
cout << s2;
}
template <typename T>
void Swap(T& x1, T& x2) {
T temp = x1;
x1 = x2;
x2 = temp;
}
template<>
void Swap<Student>(Student& s1, Student& s2) {
int temp = s1.getAge(); //写上我们的处理逻辑。仅仅交换年龄。
s1.setAge(s2.getAge());
s2.setAge(temp);
}
函数声明回顾
回顾一下,到目前为止C++函数一共有多少种声明情况:
- 1、无形参无返回值
void helloWorld() {
cout << "Hello World C++";
}
- 2、有形参无返回值
void bulleSort(int arr[], int len) {
int flag = true;
for (int i = 0; i < len-1 && flag; i++)
{
flag = false;
for (int j = len-1; j >=i; j--)
{
if (arr[i] > arr[j]) {
swap(arr[i], arr[j]);
flag = true;
}
}
}
}
- 3、可变形参
/// <summary>
/// 获取可变参数
/// </summary>
/// <param name="num">可变参数总和</param>
/// <param name="...">逗号分隔</param>
void getAverage(int num, ...) {
va_list vl;
va_start(vl, num); //num很重要,他确定栈元素个数
//由栈底往栈顶的过程..
while (num != 0) {
char* value = va_arg(vl, char*);
cout << value;
num--;
}
va_end(vl); //让指针指向空。不然后期可能会变成一个野指针。出现新的问题。
}
- 4、指针作为形参的
void getSeconds(unsigned int *par){
*par = time(NULL);
return;
}
- 5、指针作为函数返回值
int *getRandom() {
// C 语言不支持在调用函数时返回局部变量的地址,除非定义局部变量为 static 变量。
static int r[10];
srand((unsigned) time(NULL));
for (int i = 0; i < 10; ++i) {
r[i] = rand();
printf("%d\n", r[i] );
}
return r;
}
- 6、一维数组作为形参
#include <iostream>
using namespace std;
//void forEach(int arr[],int length)
void forEach(int* arr,int length) {
for (size_t i = 0; i < length; i++)
{
cout << "a[" << i << "]=" << arr[i]<<endl;
}
}
int main() {
int arr[] = { 1, 2, 3 };
forEach(arr,sizeof(arr)/sizeof(int));
}
- 7、二维数组作为形参
#include <iostream>
using namespace std;
void forEach(int p[][3],int len){ //二维数组名等价于行地址
//void forEach(int(*p)[3], int len) {
for (int i = 0; i < len; i++)
{
for (int j = 0; j < 3; j++)
{
cout << " p[" << i << "][" << j << "] = " << p[i][j];
}
cout << endl;
}
}
int main() {
int arr[][3] = { {11,12,13},{21,22,23} };
forEach(arr, 2);
}
- 8、三维数组作为形参
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
void func(int(*p)[2][3], int len) {
int number = 1;
for (int a = 0; a < len; a++)
{
for (int b = 0; b < 2; b++)
{
for (int c = 0; c < 3; c++)
{
p[a][b][c] = number++;
}
}
}
}
void forEach(int p[][2][3], int len) {
for (int a = 0; a < len; a++)
{
for (int b = 0; b < 2; b++)
{
for (int c = 0; c < 3; c++)
{
cout << p[a][b][c] << "\t";
}
cout << endl;
}
}
cout << endl << endl;
}
int main()
{
int bh[4][2][3];
memset(bh, 0, sizeof(bh));
func(bh, 4);
forEach(bh, 4);
}
- 9、结构体作为形参
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
struct Student
{
int id;
char name[20];
};
void printBook(struct Student stu) {
cout << "Student(id=" << stu.id << ",name=" << stu.name << ")";
}
int main() {
Student stu{ 1,"wangnaixing"};
printBook(stu);
}
- 10、结构体指针作为形参
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
struct Student
{
int id;
char name[20];
};
void modifyName(Student* stu) {
strcpy(stu->name, "zhangsanhahaha");
}
int main() {
Student stu{ 1,"wangnaixing"};
modifyName(&stu);
}
- 11、引用作为形参
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Student {
private:
int id;
string name;
public:
Student(const int id, const string name) :id(id), name(name) {};
void setName(const string name) {
this->name = name;
}
void setId(const int id) {
this->id = id;
}
void show() {
cout << "id=" << id << ",name" << name << endl;
}
};
void modifyStudent(Student& stu) { //感觉有规律,反正是类就用引用,是结构体就用指针
stu.setId(999);
stu.setName("遭老罪了~~~");
}
int main() {
Student stu{ 1,"wangnaixing"};
modifyStudent(stu);
stu.show();
}
- 12、结构体引用作为形参
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
struct Student {
int id;
string name;
};
void printStudentInfo(Student& student) {
student.id = 666;
student.name = "modifyName";
cout << "我是第" << student.id << "号的学生,我的名字叫" << student.name;
}
int main()
{
Student student = { 1,"wangnaixing" };
printStudentInfo(student);
}
- 13、引用作为返回值的
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
struct Student {
int id;
string name;
};
Student& func(Student& student) {
student.id = 666;
student.name = "modifyName";
return student;
}
int main()
{
Student student = { 1,"wangnaixing" };
Student modifyStudent = func(student);
cout << "我是第" << modifyStudent.id << "号的学生,我的名字叫" << modifyStudent.name;
}
- 14、函数后置处理
将返回类型移到了函数声明的后面。
auto是一个占位符(C++11给auto新增的角色), 为函数返回值占了一个位置。
int func(int x,double y);
等同:
auto func(int x,double y) -> int;
这种语法也可以用于函数定义:
auto func(int x,double y) -> int
{
// 函数体。
}
没啥意义,可能是决定返回值在前面太丑了??像JS一样弄个箭头函数去掉function?额额额,auto 这里只是一个占位符。。。人家JS用箭头函数好歹还处理了 this指针的问题呢。