C++编程中有许多易错点,下面是一些常见的错误和需要注意的事项(以后会逐渐补充在学习中遇到的错误,因为作者才大二):
-
未初始化的变量:在C++中,局部变量如果不进行初始化,它们的值将是未定义的。这可能会导致不可预测的行为。
-
数组越界:访问数组元素时,如果索引超出数组的范围,将会导致未定义行为。这通常会导致程序崩溃或更难以追踪的错误。
-
指针错误:指针操作是C++中常见的错误来源。例如,解引用空指针、野指针(已经被释放但仍然被引用的指针)、悬挂指针(指向已经被释放的内存的指针)等。
-
内存泄漏:在使用
new
动态分配内存后,如果没有使用delete
释放内存,会导致内存泄漏。长时间运行的程序可能会因此耗尽内存。 -
拷贝构造函数和赋值运算符:当自定义类时,如果没有正确实现拷贝构造函数和赋值运算符,可能会导致资源泄漏、数据不一致等问题。
-
类型转换:C++中的类型转换(特别是C风格转换)可能导致类型不匹配和未定义行为。应尽量避免使用C风格转换,而是使用C++风格的类型转换(如
static_cast
,dynamic_cast
,const_cast
,reinterpret_cast
)。 -
引用与指针混淆:引用和指针在某些情况下可以互换使用,但它们有不同的语义。不正确地使用它们可能导致程序逻辑错误。
注意,数组名作为参数传入函数的时候,本质是传的指针,即数组的首个元素的地址 -
头文件保护:在头文件中,如果没有使用预处理器宏(如
#ifndef
,#define
,#endif
)来防止头文件被多次包含,会导致编译错误。 -
函数重载与隐藏:不正确地重载函数或由于继承导致函数隐藏,可能导致调用错误的函数版本。
-
未定义的行为:C++标准中定义了一些行为是未定义的,这意味着编译器可以为此生成任何代码,或者不生成代码,而不会有任何警告或错误。例如,除以零、数组越界访问、类型别名等。
-
浮点数精度问题:浮点数运算可能会引入舍入误差,这在需要精确计算时可能导致问题。
-
多线程竞争条件:在多线程程序中,如果没有正确使用同步机制(如互斥锁、条件变量等),可能会导致数据竞争和其他并发问题。
-
异常安全:在编写可能抛出异常的函数时,需要确保函数在异常发生时能够保持资源的一致性。否则,可能会导致资源泄漏或其他问题。
-
STL容器使用错误:在使用STL(标准模板库)容器时,需要了解容器的特性、迭代器失效等问题,否则可能会导致程序错误。
-
使用未定义的变量或函数:在引用变量或函数之前,必须确保它们已经被定义和声明。
-
类型不匹配:在进行算术运算或赋值操作时,需要确保操作数的类型匹配,否则可能会导致编译错误或运行时错误。
-
忽视返回值:一些函数会返回重要的信息(如状态码、指针等),如果忽视这些返回值,可能会导致程序逻辑错误。
-
忽视编译器警告:编译器警告通常是潜在问题的指示。即使程序能够编译和运行,也应该仔细检查并解决所有警告。
-
构造函数不能像成员函数一样被调用
必须初始化常量和引用,因此不能默认构造包含const或引用成员的类。 -
静态成员变量必须在类外初始化,且必须要初始化。
class Myclass{static int sum;// static data member};int Myclass ::sum=8; // initialization静态成员函数只能访问静态成员变量
类的对象都可以访问静态成员,但是所有类的成员访问的静态成员都是同一份静态成员;
静态成员在类内部声明时加static声明,在类外定义静态成员时无需添加static修饰
静态成员的本质就是全局变量和全局函数,在类中用static声明,相当于把类和这个全局变量/函数进行绑定;
普通成员函数可以调用静态成员变量和函数;
静态函数只能访问静态函数和静态变量,不能访问任何非静态的东西;
在对象的构造函数中不能对静态变量进行初始化,因为静态变量不属于单个对象,不需要对象去初始化;
因为静态成员是全局变量/函数,生命周期是整个程序的生命周期,所以我们不需要创建对象就可以使用静态成员;
虚函数不能是静态成员函数; -
尽量避免使用预处理宏
#define f(x) x*xint main( ) {int x(2);cout << f(x) << endl;cout << f(x+1) << endl;return 0;}//在这段代码中,第二行会输出5(2+1*2+1) -
内联函数
内联函数定义必须在调用之前
内联函数不能递归调用
内联函数的主体不包括异常处理
在类内定义的函数都是内联函数,在类内声明,类外定义的函数要加上inline关键字才是内联函数
“内联”关键字在类定义中不是必需的
在类定义中定义的任何函数都自动为内联函数。 - 如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。
析构函数本身并不直接释放堆区的数据。但是,析构函数通常用于执行与对象销毁相关的清理操作,这可以包括释放对象可能拥有的任何动态分配的资源,如堆上分配的内存。
class MyClass {
public:
MyClass() {
// 使用new在堆上分配内存
data = new int[10];
}
~MyClass() {
// 在析构函数中释放堆上分配的内存
delete[] data;
}
private:
int* data;
};
int main() {
MyClass* obj = new MyClass();
// ... 使用obj ...
delete obj; // 调用MyClass的析构函数,然后释放obj本身占用的内存
return 0;
} -
函数返回类型为引用或指针时要判断返回的内容是否有效,例如不能返回函数体内的局部变量,因为局部变量在函数调用完后自动释放了
int& h( )
{
int q = 10;
return q; // Error
}
int* h( )
{
int q = 20;
return &q; // Error
}
25.子类不能继承父类的构造函数和析构函数
如果基类具有构造函数,则必须由派生类调用构造函数。注意如果基类具有构造函数,而派生类没有显式地调用基类的构造函数,编译器会尝试隐式地调用基类的默认构造函数(如果存在的话)。如果基类没有定义默认构造函数,编译器将无法生成派生类对象,因为它不知道如何初始化基类的数据成员。为了避免这种情况,如果基类具有构造函数(无论是默认构造函数还是带参数的构造函数),派生类应该在其初始化列表中显式地调用基类的构造函数。这样可以确保基类的数据成员在派生类对象创建之前得到正确的初始化。
// 只能接受MyClass<int>类型的对象
}
void myFunction(MyClass<T> obj) {
// 使用obj...
}//函数模板可以自动推导类型
};
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
如果用到this指针,需要加以判断保证代码的健壮性
36.初始化列表能只能初始化一次,多次初始化会报错
37.运算符重载
加号运算符可以成员函数实现也可以全局函数实现
//成员函数实现 + 号运算符重载
Person operator+(const Person& p) {
Person temp;
temp.m_A = this->m_A + p.m_A;
temp.m_B = this->m_B + p.m_B;
return temp;
}
/全局函数实现 + 号运算符重载
//Person operator+(const Person& p1, const Person& p2) {
// Person temp(0, 0);
// temp.m_A = p1.m_A + p2.m_A;
// temp.m_B = p1.m_B + p2.m_B;
// return temp;
//}
左移运算符
ostream& operator<<(ostream& out, Person& p) { out << "a:" << p.m_A << " b:" << p.m_B; return out; }//这里的out相当于cout的别名,return out是为了实现链式编程
前置与后置++
MyInteger& operator++() {
//先++
m_Num++;
//再返回
return *this;
}
//后置++
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}
38.向上类型转换
类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型( Upcasting )。相应地,将基类赋值给派生类称为向下转型( Downcasting )。
向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预
39.
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
- 常函数注意事项:
- 1)不能更新类的成员变量
2)不能调用该类中没有用const修饰的成员函数,即只能调用常成员函数
3)可以被类中其它的成员函数调用
4)常对象只能调用常成员函数,而不能调用其他的成员函数
5)const是函数类型的一部分,在实现部分也要带const。 - 只能通过构造函数的初始化参数列表对常成员变量进行初始化
40重载赋值运算符与深浅拷贝 - 深拷贝与浅拷贝:对于包含动态分配内存的类,如果直接使用默认的赋值运算符,可能会导致浅拷贝(只复制指针,不复制实际的数据),这可能会引发悬挂指针、双重释放等问题。通过重载赋值运算符,你可以实现深拷贝,确保每个对象都有自己独立的数据副本。
41.同一种类之间可以互相访问私有成员变量#include<iostream>
using namespace std;class Point {
public :
Point ( int xx = 0 , int yy = 0 )
{ X = xx ; Y = yy ; }void shuchu(const Point & p )
{
cout<<p.X<<endl;
}
private : int X , Y ;
};
int main ( ) {
Point A ( 1 , 2 ) ;
Point B(3,4);
A.shuchu(B);
cout<<B.X<<endl;
return 0;}
41C++提供了两种主要的机制来实现类的类型转换:构造函数和类型转换函数(或称为转换运算符)。
1. 构造函数作为类型转换
在某些情况下,构造函数可以被用来执行隐式类型转换。然而,由于这可能会导致代码难以阅读和维护,因此通常建议避免在构造函数中提供隐式类型转换。但如果你确实需要这样的行为,可以通过在构造函数声明中省略
explicit
关键字来实现。cpp复制代码
class MyClass {
public:
MyClass(int value) { // 隐式构造函数,可用于隐式类型转换
// ...
}
// ...
};
MyClass obj = 42; // 隐式类型转换,通过调用MyClass(int)构造函数
但是,为了避免意外的类型转换,通常建议将这样的构造函数声明为
explicit
,以禁止隐式类型转换。cpp复制代码
class MyClass {
public:
explicit MyClass(int value) { // 显式构造函数,不能用于隐式类型转换
// ...
}
// ...
};
MyClass obj = 42; // 错误:不能使用隐式构造函数进行类型转换
MyClass obj2(42); // 正确:使用显式构造函数进行类型转换
2. 类型转换函数(转换运算符)
类型转换函数(也称为转换运算符)是一种特殊的成员函数,它允许类定义自己的类型转换规则。这种函数通常用于将类的对象转换为其他类型。类型转换函数没有返回类型(返回类型由函数名决定),并且函数名必须是
operator
后跟目标类型。cpp复制代码
class MyClass {
public:
operator int() const { // 类型转换函数,将MyClass对象转换为int
// ...
return some_int_value;
}
// ...
private:
int some_int_value;
};
MyClass obj;
int value = obj; // 使用类型转换函数将MyClass对象转换为int
3. 类型转换的注意事项
- 隐式类型转换可能会导致代码难以阅读和维护,因此通常建议避免使用隐式构造函数进行类型转换,除非有明确的理由。
- 显式构造函数和类型转换函数都可以提供显式的类型转换机制,但它们的用途和上下文可能有所不同。
- 在设计类时,应该仔细考虑是否需要提供类型转换功能,并确保这种功能不会导致意外的行为或副作用。
- 如果类提供了多个可能导致混淆的类型转换函数,可能会导致编译错误或运行时错误。在这种情况下,应该仔细考虑类的设计和类型转换函数的实现。
要避免这些错误,需要深入理解C++语言特性和编程实践,并遵循良好的编码习惯和规范。同时,使用调试工具、编译器警告和静态分析工具可以帮助发现潜在的问题。