在C语言中,可以用以下两种方式进行显式类型转换:
(T) exp
:在这种方式中,你将把表达式(exp)转换成类型T。这种类型转换方法被称为 “C风格的类型转换” 或者 “传统的类型转换”。
T(exp)
:在这种方式中,你也将把表达式(exp)转换成类型T。这种类型转换方法在C++中更常见,并被称为 “函数风格的类型转换” 或者 “构造函数的类型转换”。
然而,这两种方式在C语言中的效果是一样的,即将表达式exp的类型转换成T。例如,如果你有一个整数 int i = 10;
并且你想将它转换为浮点数,你可以这样做: (float) i
或者 float(i)
。
但是需要注意,虽然这两种方式在C语言中都可以使用,但是函数风格的类型转换T(exp)
在C++中更常见。如果你正在编写的是C程序,那么建议使用(T) exp
,因为这种方式在所有的C编译器中都可以被接受。
最后,我想强调的是,无论是哪种类型转换方式,都应该谨慎使用。在不同类型之间进行转换可能会导致数据丢失或者精度损失。例如,将浮点数转换为整数,小数部分会被丢弃;而如果将大的整数转换为小的整数类型,可能会导致数据溢出。因此,在编写程序时,一定要确保你明白为什么需要进行类型转换,以及转换可能会带来什么样的影响。
在C++中,提供了四种显式类型转换运算符,分别是 static_cast
,const_cast
,dynamic_cast
和 reinterpret_cast
。每一种类型转换运算符都有其特定的使用场景和规则。
static_cast
:
这是最常见的类型转换方法,能够在任何数据类型之间进行转换,包括指针类型。然而,它不能将const对象转换为非const对象。除此之外,当你对指针进行static_cast
时,必须确保转换的类型是安全的,因为这种类型转换没有运行时类型检查。
const_cast
:
const_cast
主要用于修改类型的const或volatile属性。比如,它可以将一个const
指针转换为非const
指针,或者将const
对象转换为非const
对象。需要注意的是,const_cast
并不能改变底层数据,只是改变了对数据的访问权限。
dynamic_cast
:
dynamic_cast
主要用于安全地向下转型(在继承层次中从基类向派生类转型)。也就是说,它可以在运行时进行类型检查,如果转换是非法的,那么转换的结果将会是nullptr
。但这种类型转换只能用于含有虚函数的类,因为只有这样的类才能进行运行时类型检查。
bad_cast
异常,指针转换失败返回的是nullptr
reinterpret_cast
:
reinterpret_cast
是最不安全的类型转换方法,它能进行任意类型之间的转换,包括指针和整数之间的转换。然而,使用reinterpret_cast
可能会产生非法的数据,所以除非在必要的时候才应该使用。
在C语言中,常见的类型转换主要是数值类型之间的转换。例如:
int i = 10;
double d = (double)i; // 将整数i转换为double类型
在这个例子中,我们使用了(double)i
进行了类型转换,将整数i
转换为了double
类型。
在C++中,类型转换更为复杂。以下是使用static_cast
,const_cast
,dynamic_cast
和reinterpret_cast
的例子:
static_cast
:int i = 10;
double d = static_cast<double>(i); // 将整数i转换为double类型
这个例子中,我们使用了static_cast
进行了类型转换,将整数i
转换为了double
类型。
const_cast
:const int i = 10;
int* pi = const_cast<int*>(&i); // 将const int指针转换为非const int指针
在这个例子中,我们使用了const_cast
进行了类型转换,将const int
指针转换为了非const int
指针。
dynamic_cast
:class Base {
virtual void foo() {}
};
class Derived : public Base {
void foo() override {}
};
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 安全地将Base*转换为Derived*
在这个例子中,我们使用了dynamic_cast
进行了类型转换,安全地将Base*
转换为了Derived*
。
reinterpret_cast
:int i = 10;
int* pi = &i;
char* pc = reinterpret_cast<char*>(pi); // 将int指针转换为char指针
在这个例子中,我们使用了reinterpret_cast
进行了类型转换,将int*
转换为了char*
。
空的类定义,不会生成构造函数,没有意义,即便里面包含成员变量,成员变量时基础类型,还是不会生成默认构造函数,编译器也不知道用什么值去初始化
在C++中,编译器会为类生成一个默认的拷贝构造函数(也就是一个隐式的拷贝构造函数)的情况是,只要你没有为这个类明确地定义一个拷贝构造函数。这个默认的拷贝构造函数将执行每个成员的拷贝,这通常意味着执行成员的拷贝构造函数(对于类类型的成员),或者执行简单的位拷贝(对于内置类型的成员)。
这个行为是由 C++ 的标准所定义的。默认的拷贝构造函数通常是足够的,除非你的类需要管理自己的资源,比如动态分配的内存。在这种情况下,你需要定义你自己的拷贝构造函数,以正确地执行深拷贝,否则可能会出现问题(例如,浅拷贝导致的资源的多次删除)。
需要注意的是,如果你定义了任何其他的构造函数(比如移动构造函数或参数化的构造函数),但没有定义拷贝构造函数,编译器仍然会为你生成默认的拷贝构造函数。只有当你明确地定义了拷贝构造函数时,编译器才不会生成默认的拷贝构造函数。
同2.2
初始化:当一个对象以另一个对象作为初始值时,会发生拷贝操作。例如,如果你声明了一个新的对象并以另一个对象作为初始值,那么会发生拷贝操作:
MyClass obj1;
MyClass obj2 = obj1; // 拷贝操作
函数参数传递:当一个对象作为函数参数时,如果这个参数是按值传递的,那么在函数被调用时会发生拷贝操作:
void foo(MyClass obj) { /* ... */ }
MyClass obj;
foo(obj); // 调用 foo 时,obj 会被拷贝
函数返回:当一个函数返回对象时,如果这个对象是按值返回的,那么在函数返回时会发生拷贝操作:
MyClass foo() {
MyClass obj;
return obj; // 返回时,obj 会被拷贝
}
赋值:当一个对象被赋予另一个对象的值时,会发生拷贝操作:
MyClass obj1;
MyClass obj2;
obj2 = obj1; // 拷贝操作
作为容器元素:当一个对象被插入到一个容器(例如,std::vector
、std::list
等)时,会发生拷贝操作:
std::vector<MyClass> vec;
MyClass obj;
vec.push_back(obj); // obj 被拷贝到 vec 中
浅拷贝意味着仅复制对象的成员值,而不考虑这些值是否表示其他资源的引用。如果一个对象有指向动态分配内存或其他资源的指针,浅拷贝只会复制这个指针的值(即复制了内存地址),而不会复制该指针所指向的资源。
这可能会导致问题,因为当多个对象共享同一个资源时,一旦其中一个对象在析构时释放了这个资源,其他对象就会持有一个无效的指针。这称为悬挂指针(dangling pointer)问题。
以下是一个简单的浅拷贝的例子:
class ShallowCopyExample {
int* data;
public:
ShallowCopyExample(int value) {
data = new int;
*data = value;
}
// 浅拷贝复制构造函数
ShallowCopyExample(const ShallowCopyExample& source) {
data = source.data;
}
~ShallowCopyExample() {
delete data;
}
};
相对的,深拷贝不仅复制对象的成员值,而且会为任何动态分配的资源创建新的副本。也就是说,如果一个对象有指向动态分配内存的指针,深拷贝会创建这段内存的一个新副本,并将新对象的指针指向这个新的内存。
这样做可以避免悬挂指针问题,因为每个对象都有自己的资源,不会和其他对象共享。
以下是一个简单的深拷贝的例子:
class DeepCopyExample {
int* data;
public:
DeepCopyExample(int value) {
data = new int;
*data = value;
}
// 深拷贝复制构造函数
DeepCopyExample(const DeepCopyExample& source) {
data = new int; // 为新对象分配内存
*data = *source.data; // 复制资源
}
~DeepCopyExample() {
delete data; // 释放对象的资源
}
};
需要注意的是,C++默认的复制构造函数和赋值运算符都是执行浅拷贝的。如果你的类管理动态分配的资源,你需要重写自己的复制构造函数和赋值运算符,以实现深拷贝。
局部的const
局部const是指在函数内部声明的const变量。这种变量只在声明它的函数体内部可见,且在其作用域内其值不能被改变。
例如:
void func() {
const int x = 10; // x是局部const
}
全局的const
全局const是指在函数外部声明的const变量。这种变量在整个程序中都是可见的,且其值不能被改变。
例如:
const int y = 20; // y是全局const
void func() {
// 可以在这里使用y
}
符号表
符号表是编译器用来存储程序中使用的所有变量、函数等标识符的信息。对于const变量来说,它们的值和类型都会被存储在符号表中。有如下的作用:
类型检查:编译器通过查阅符号表中的类型信息来进行类型检查。如果一个 const
变量被用于一个需要不同类型的表达式中,编译器可以发出警告或错误。
值替换和优化:由于 const
变量的值在编译时就已知,所以编译器可以在编译和优化过程中直接使用这些值。例如,如果你有一个表达式 const int x = 5; int y = x * 2;
,编译器可能直接将其优化为 int y = 10;
。这可以提高运行时的性能。
保护 const
变量的值:由于 const
变量的值在符号表中,编译器可以确保在程序中的任何地方都不会改变这个值。如果试图修改一个 const
变量的值,编译器会发出错误。
常量指针
常量指针是指向const对象的指针,也就是说,你不能通过这种指针修改它所指向的值,但是你可以改变这个指针所指向的地址。
例如:
const int x = 10;
const int* p = &x;
在这个例子中,p
是一个指向const int的指针,所以你不能通过p
来改变x
的值。
指针常量
指针常量是一种你不能改变其所指向的地址的指针,但是你可以通过这种指针修改它所指向的值。
例如:
int x = 10;
int* const p = &x;
在这个例子中,p
是一个const指针,所以你不能改变p
的值(也就是说,你不能让p
指向其他的地址),但是你可以通过p
来改变x
的值。
6.作用
在 C++ 中,const
关键字修饰函数参数有以下几个主要作用:
防止参数值被修改:当你使用const
关键字修饰一个函数参数时,你就不能在函数体内修改该参数的值。这可以防止函数中的错误修改。
void foo(const int x) {
x = 42; // 编译错误,因为x是const
}
实现接口承诺:const
参数可以向使用函数的程序员传达重要信息。它明确表明函数不会修改参数的值。这是一种很好的自我文档,并能帮助防止使用函数的人产生错误。
提高函数的通用性:const
引用或指针参数允许函数接受const
和非const
实参,而非const
引用或指针只能接受非const
实参。这使得函数可以在更多的上下文中被使用。
void bar(const std::string& str) {
// str 是 const 引用,不能在函数体内被修改
}
const std::string str1 = "Hello";
std::string str2 = "World";
bar(str1); // 有效,因为str1是const
bar(str2); // 也有效,因为非const实参可以转换为const引用
提高性能:对于大对象,通过const
引用传递参数可以避免复制操作,同时保证函数不会修改参数。
需要注意的是,对于基本数据类型(如 int
、float
等),以值传递参数并使用 const
修饰通常并不会带来实质性的性能优势,因为复制这些类型的代价很小。然而,对于大型类和结构,通过 const
引用传递参数可以带来显著的性能优势。
在 C++ 中,const
关键字修饰函数返回值主要有以下几个作用:
返回对象的只读版本:如果函数返回一个对象的引用或指针,并且你不希望调用者通过这个引用或指针修改这个对象,你可以使用const
关键字来修饰返回类型。这意味着函数返回的引用或指针是只读的,不能用来修改对象。
const int& foo() {
static int x = 0;
return x;
}
在这个例子中,foo
函数返回一个对int
的const
引用,这意味着你不能通过这个引用来修改x
。
防止意外的对象修改:const
关键字能防止你在无意中修改了不应该修改的对象。这是一种安全机制,能够帮助你避免一些可能导致错误的行为。
增强接口语义:在某些情况下,const
关键字能够提供更清晰的语义。例如,如果你有一个成员函数,这个函数返回一个成员变量的引用,并且这个函数不应该改变对象的状态,你可能会选择返回一个const
引用。
需要注意的是,对于返回一个局部对象的函数,返回类型通常不应该是const
引用或指针,因为当函数返回后,局部对象就不存在了。在这种情况下,你应该返回一个值或者一个指向动态分配内存的指针。
注意点
作用
野指针和悬挂指针的区别
野指针(Wild Pointer)通常是指任何未初始化或者未正确设置的指针,而悬挂指针(Dangling Pointer)通常是指指向已经释放或者超出生命周期的对象的指针。
运算符重载
在 C++ 中,运算符重载是一种使你能够改变某种类型(特别是用户定义的类型)的运算符行为的语言特性。你可以通过定义特殊的成员函数或全局函数来重载运算符。
以下是一个重载 +
运算符的例子。我们定义了一个 Complex
类,它表示复数,然后重载 +
运算符来实现复数的加法。
class Complex {
public:
Complex(double real, double imag) : real_(real), imag_(imag) {}
double real() const { return real_; }
double imag() const { return imag_; }
Complex operator+(const Complex& other) const {
return Complex(real_ + other.real_, imag_ + other.imag_);
}
private:
double real_;
double imag_;
};
int main() {
Complex a(1.0, 2.0);
Complex b(3.0, 4.0);
Complex c = a + b; // 使用我们重载的 + 运算符
// c.real() 的值为 4.0,c.imag() 的值为 6.0
return 0;
}
注意以下几点:
+
和 *
运算符,*
运算符仍然比 +
运算符有更高的优先级。.
(成员访问)运算符、::
(作用域解析)运算符、sizeof
运算符和 ?:
(条件)运算符不能被重载。&&
运算符执行乘法的重载。+
或 -
),你可以选择将其重载为成员函数或全局函数。如果你选择将其重载为成员函数,那么第一个操作数就是调用该函数的对象。如果你选择将其重载为全局函数,那么你需要为两个操作数都提供参数。