杭州正晖建设工程有限公司网站网络营销事件案例
web/
2025/9/26 13:06:06/
文章来源:
杭州正晖建设工程有限公司网站,网络营销事件案例,网站建设 容易吗,网站建设的开发程序81.C中的组合和继承相比的优缺点
在C中组合一对象系用描述对象包对象系组一个拥对象例其变合类的含的现。这的量类当有员被创建。
以下一个示例#xff0c;展示了在C中如何实现组合关系#xff1a;
class Engine {// Engine class definition...
};class Car {Engine engi…81.C中的组合和继承相比的优缺点
在C中组合一对象系用描述对象包对象系组一个拥对象例其变合类的含的现。这的量类当有员被创建。
以下一个示例展示了在C中如何实现组合关系
class Engine {// Engine class definition...
};class Car {Engine engine;// Other member variables and functions...
};int main() {Car car;// Access cars engine through the member variable:car.engine.start(); // Example method callreturn 0;
}在上面的示例中Car 类中的 engine 成员变量是一个 Engine 类的对象。通过组合Car 类中的每个对象都拥有一个引擎对象作为其一部分。在主函数中我们可以通过 car.engine 来访问 Car 对象的引擎并调用相关方法。
需要注意的是组合关系不同于继承关系。在组合中对象之间的关系是 “has-a”而在继承中对象之间的关系是 “is-a”。组合关系更多地体现了对象之间的整体与部分的关系。
组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。
组合的优点
①当前对象只能通过所包含的那个对象去调用其方法所以所包含的对象的内部细节对当前对象时不可见的。
②当前对象与包含的对象是一个低耦合关系如果修改包含对象的类中代码不需要修改当前对象类的代码。
③当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。
组合的缺点①容易产生过多的对象。②为了能组合多个对象必须仔细对接口进行定义。
继承
继承是Is a 的关系比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。
继承的缺点有以下几点
①父类的内部细节对子类是可见的。
②子类从父类继承的方法在编译时就确定下来了所以无法在运行期间改变从父类继承的方法的行为。
③如果对父类的方法做了修改的话比如增加了一个参数则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合违背了面向对象思想。
85.函数调用过程栈的变化返回值和参数变量哪个先入栈
1、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;
2、调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);
3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);
4、在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;
对于大部分的编程语言和操作系统被调用函数通常会在进入函数之前保存调用者函数的栈底地址也称为帧指针Frame Pointer和栈顶地址。
下面是一般的函数调用过程 当一个函数被调用时调用者函数的指令执行流会被跳转到被调用函数的入口点。 在被调用函数中首先会将调用者函数的栈底地址保存到栈中这个操作一般由 push ebp 指令完成。 接下来将当前栈顶地址esp保存到ebp寄存器中以成为新的栈底地址。这个过程使用 mov ebp, esp 指令完成。 这样被调用函数就保存了调用者函数的栈底地址并且在ebp寄存器中保存了新的栈底地址可以使用ebp来访问函数参数和局部变量。 函数执行完成后会使用 pop ebp 指令将保存的栈底地址还原为调用者函数的栈底地址以返回到调用者函数。
这种操作可以建立一个新的函数执行帧使得被调用函数能够正确地访问参数和局部变量并在函数执行完毕后能够正确返回到调用者函数的执行位置。
需要注意的是上述过程是一种常见的方式但并非所有编程语言和操作系统都采用相同的方式来管理函数调用的栈帧。具体的实现可能会有所不同。
在典型的函数调用过程中被调函数通常会使用栈来存放局部变量和临时变量。栈的延伸方向可以是向下或向上取决于特定的体系结构和操作系统约定。在大多数情况下栈的延伸方向是向下的也就是地址依次减小。
对于采用向下延伸的栈函数中的局部变量和临时变量会从ebp指针处往下分配内存空间并按照变量定义的顺序依次排列。具体步骤如下 将ebp的值保存到临时寄存器通常使用mov指令将ebp的值存储到esp或者eax等寄存器。 将esp的值赋给ebp将ebp的值设置为当前的栈顶地址即将ebp指向当前函数的栈帧底部。 为该函数的局部变量和临时变量分配内存空间。根据变量的类型和大小在ebp的位置逐渐减小的方向上分配适当的内存空间。 变量根据定义的顺序依次被分配在栈上先定义的变量会在栈中较低的位置。 在函数的执行过程中可以通过使用ebp加上偏移量来访问局部变量和临时变量。
需要注意的是这种栈帧的实现方式是一种常见的做法但并非所有的编程语言和操作系统都采用相同的方式。具体的实现可能会根据编程语言、编译器或操作系统的不同而有所变化。
91、你知道重载运算符吗
1、 我们只能重载已有的运算符而无权发明新的运算符对于一个重载的运算符其优先级和结合律与内置类型一致才可以不能改变运算符操作数个数
2、 两种重载方式成员运算符和非成员运算符成员运算符比非成员运算符少一个参数下标运算符、箭头运算符必须是成员运算符
3、 引入运算符重载是为了实现类的多态性
4、 当重载的运算符是成员函数时this绑定到左侧运算符对象。成员运算符函数的参数数量比运算符对象的数量少一个至少含有一个类类型的参数
5、 从参数的个数推断到底定义的是哪种运算符当运算符既是一元运算符又是二元运算符-*
6、 下标运算符必须是成员函数下标运算符通常以所访问元素的引用作为返回值同时最好定义下标运算符的常量版本和非常量版本
7、 箭头运算符必须是类的成员解引用通常也是类的成员重载的箭头运算符必须返回类的指针
对第2条的解释 C 中的运算符重载可以使用两种方式成员运算符重载和非成员运算符重载。它们在语法上存在一些差异其中成员运算符重载比非成员运算符重载少一个参数。
成员运算符重载使用类的成员函数作为运算符重载函数通过成员函数来定义运算符的行为。在成员运算符重载中运算符重载函数将被重载的运算符作为成员函数的一部分因此成员运算符重载函数只需一个参数。
例如可以通过重载类的成员函数 operator() 来实现两个对象的相加运算
class MyClass {
public:int value;MyClass() : value(0) {}MyClass operator(const MyClass other) {MyClass result;result.value this-value other.value;return result;}
};MyClass obj1;
obj1.value 10;
MyClass obj2;
obj2.value 20;
MyClass sum obj1 obj2; // 使用成员运算符重载非成员运算符重载使用独立于类的函数实现运算符重载。在非成员运算符重载中重载函数不是类的成员函数在函数参数中需要显式传递操作数。因此非成员运算符重载函数需要两个参数一个或两个操作数。
例如在上面的示例中可以使用非成员函数来实现相加运算符的重载
class MyClass {
public:int value;MyClass() : value(0) {}
};MyClass operator(const MyClass obj1, const MyClass obj2) {MyClass result;result.value obj1.value obj2.value;return result;
}MyClass myObj1;
myObj1.value 10;
MyClass myObj2;
myObj2.value 20;
MyClass sum myObj1 myObj2; // 使用非成员运算符重载注意在实现非成员运算符重载时我们没有使用成员函数而是使用了一个独立的函数并将对象作为参数进行操作。
无论是成员运算符重载还是非成员运算符重载都可以实现对运算符的自定义行为但使用成员运算符重载时函数只接收一个操作数而使用非成员运算符重载时函数需要显式传递操作数。选择哪种方式主要取决于设计需求和语义上的差异。
131、介绍一下几种典型的锁
读写锁
多个读者可以同时进行读 写者必须互斥只允许一个写者写也不能读者写者同时进行 写者优先于读者一旦有写者则后续读者必须等待唤醒时优先考虑写者 互斥锁
一次只能一个线程拥有互斥锁其他线程只有等待
互斥锁是在抢锁失败的情况下主动放弃CPU进入睡眠状态直到锁的状态改变时再唤醒而操作系统负责线程调度为了实现锁的状态发生改变时唤醒阻塞的线程或者进程需要把锁交给操作系统管理所以互斥锁在加锁操作时涉及上下文的切换。互斥锁实际的效率还是可以让人接受的加锁的时间大概100ns左右而实际上互斥锁的一种可能的实现是先自旋一段时间当自旋的时间超过阀值之后再将线程投入睡眠中因此在并发运算中使用互斥锁每次占用锁的时间很短的效果可能不亚于使用自旋锁
条件变量
互斥锁一个明显的缺点是他只有两种状态锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足他常和互斥锁一起使用以免出现竞态条件。当条件不满足时线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制条件变量则是同步机制。
自旋锁
如果进线程无法取得锁进线程不会立刻放弃CPU时间片而是一直循环尝试获取锁直到获取为止。如果别的线程长时期占有锁那么自旋就是在浪费CPU做无用功但是自旋锁一般应用于加锁时间很短的场景这个时候效率比较高。
条件变量Condition Variable常与互斥锁Mutex一起使用以避免竞态条件Race Condition的发生。
竞态条件是指多个线程并发执行时由于执行顺序不确定而导致结果的正确性受到影响的情况。当多个线程访问共享资源时如果没有适当的同步机制就可能引发竞态条件。
互斥锁用于保护共享资源的访问它可以确保在任意时刻只有一个线程能够访问被保护的资源从而避免竞态条件的发生。
但有些情况下仅使用互斥锁可能不足以满足程序的需求。例如当一个线程等待某个条件达成后再继续执行就需要使用条件变量。
条件变量允许线程在满足特定条件之前阻塞并等待其他线程发出的信号。它通常与互斥锁一起使用以确保线程的等待和唤醒操作在互斥的情况下进行。
在使用条件变量时通常会遵循以下模式 线程获取互斥锁。 检查条件是否满足如果不满足则进入等待状态释放互斥锁。 其他线程改变条件并发出信号。 等待的线程被唤醒后重新获取互斥锁并检查条件是否满足。 如果条件仍未满足则回到步骤2继续等待。
使用条件变量可以确保线程在等待特定条件时被正确地阻塞并在条件满足时被正确地唤醒。这样互斥锁和条件变量的组合可以有效地避免竞态条件的出现。
内存泄漏和内存溢出的场景
下面列举了五种常见的 C 内存泄漏和内存溢出的场景
忘记释放动态分配的内存当使用 new 操作符动态分配内存后如果忘记使用 delete 操作符释放内存就会导致内存泄漏。例如
int* ptr new int;
// 忘记释放内存导致内存泄漏循环引用当存在两个或多个对象相互引用时且这些对象使用动态分配的内存若没有正确地释放这些对象会导致内存泄漏。例如
class A {
public:B* b;
};class B {
public:A* a;
};A* obj1 new A;
B* obj2 new B;
obj1-b obj2;
obj2-a obj1;
// 忘记释放 obj1 和 obj2导致内存泄漏被遗漏的析构函数当类中定义了资源管理的逻辑如打开文件、分配内存等但没有正确实现析构函数来释放这些资源会导致内存泄漏。例如
class Resource {
public:Resource() {// 打开文件或分配内存等资源的初始化操作}// 没有实现析构函数来释放资源
};Resource* res new Resource;
// res 对象没有被正确地释放导致资源未被释放指针迭代器使用错误当使用指针迭代器如 STL 容器的迭代器时若指针没有正确地增加或减少可能导致指针访问越界造成内存溢出。例如
std::vectorint vec{1, 2, 3, 4, 5};
auto it vec.begin();
it;
it;
// 忘记检查迭代器是否越界并导致访问了无效的内存递归深度太大在使用递归函数时如果递归深度过大可能导致栈溢出。每次函数调用会将函数的局部变量、返回地址等信息保存在栈上当递归层级过多时栈空间可能会被耗尽。例如
void recursiveFunction() {// ...recursiveFunction();
}recursiveFunction();
// 递归层级过多可能导致栈溢出这些是可能导致 C 内存泄漏和内存溢出的一些常见场景需要注意在编码中避免这些问题并及时释放动态分配的内存和资源。使用智能指针、RAII 等技术可以帮助更好地管理内存。
C内存分配
在C中内存分成5个区他们分别是堆、栈、全局/静态存储区和常量存储区和代码区。
栈在执行函数时函数内局部变量的存储单元都可以在栈上创建函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中效率很高但是分配的内存容量有限。堆就是那些由new分配的内存块他们的释放编译器不去管由我们的应用程序去控制一般一个new就要对应一个delete。如果程序员没有释放掉那么在程序结束后操作系统会自动回收。全局/静态存储区内存在程序编译的时候就已经分配好这块内存在程序的整个运行期间都存在。它主要存放静态数据局部static变量全局static变量、全局变量和常量。常量存储区这是一块比较特殊的存储区他们里面存放的是常量字符串不允许修改。代码区存放程序的二进制代码
63.重载实现编译时多态虚函数实现运行时多态
多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术赋值之后父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单一句话允许将子类类型的指针赋值给父类类型的指针 实现多态有二种方式覆盖override重载overload。
覆盖是指子类重新定义父类的虚函数的做法。
重载是指允许存在多个同名函数而这些函数的参数表不同或许参数个数不同或许参数类型不同或许两者都不同。例如基类是一个抽象对象——人那教师、运动员也是人而使用这个抽象对象既可以表示教师、也可以表示运动员。
重载实现编译时多态和虚函数实现运行时多态是面向对象编程中常用的两种多态性实现方式。
重载实现编译时多态 重载是指在同一个作用域内根据不同的参数类型或参数个数定义多个同名函数。编译器在编译时根据函数调用的参数类型来确定调用哪个重载函数这称为编译时多态。编译时多态可以根据不同参数类型的函数来执行不同的操作提供了灵活的编程方式。
例如
void process(int value) {// 处理整型参数的函数
}void process(float value) {// 处理浮点型参数的函数
}int main() {int a 10;float b 2.5f;process(a); // 调用处理整型参数的函数process(b); // 调用处理浮点型参数的函数return 0;
}虚函数实现运行时多态
虚函数是在基类中声明为虚函数并由其派生类重写的函数。通过基类指针或引用调用虚函数时根据对象实际的动态类型来确定调用哪个重写的函数这称为运行时多态。运行时多态在运行时根据对象的实际类型动态地选择调用哪个函数提供了灵活的面向对象编程方式。
例如
class Shape {
public:virtual void draw() {cout 绘制形状 endl;}
};class Circle : public Shape {
public:void draw() override {cout 绘制圆形 endl;}
};class Rectangle : public Shape {
public:void draw() override {cout 绘制矩形 endl;}
};int main() {Shape* shape1 new Circle();Shape* shape2 new Rectangle();shape1-draw(); // 调用Circle类的draw函数shape2-draw(); // 调用Rectangle类的draw函数delete shape1;delete shape2;return 0;
}在上述例子中通过定义基类Shape和派生类Circle和Rectangle并将派生类重写基类中的虚函数draw。然后通过指向基类的指针shape1和shape2在运行时根据实际类型调用相应的draw函数实现了运行时多态性。
64. 成员初始化列表的概念为什么用它会快一些
成员初始化列表是在类的构造函数中使用初始化列表来初始化类成员变量的一种方式。
使用成员初始化列表可以带来一些性能上的优势主要有以下几点 避免了多余的默认构造和析构过程在构造函数中如果类成员变量是通过赋值来初始化的会先调用默认构造函数创建一个临时对象然后再调用赋值操作符将临时对象的值赋给成员变量。而使用初始化列表可以直接在对象创建的过程中将初值传递给成员变量避免了多余的构造和析构操作提高了效率。 对于 const 成员变量和引用类型成员变量只能通过初始化列表来初始化const 成员变量在对象创建后就不能被修改引用类型成员变量必须在创建时绑定到一个对象。使用初始化列表可以直接在对象创建时提供初值确保 const 成员变量和引用类型成员变量的正确初始化。 一些复杂的成员对象可能没有默认构造函数或者为其提供默认参数是不可行的有些类需要非默认的构造函数来接收参数或者执行特定的初始化过程。如果通过赋值操作符来初始化这些成员对象可能会遇到没有默认构造函数的问题。使用初始化列表可以在构造函数中直接调用相应的构造函数来初始化这些复杂的成员对象。
总的来说成员初始化列表可以提高初始化的效率避免多余的操作并且对于 const 成员变量和引用类型成员变量是必须的。它是一种推荐的初始化方式特别是在构造函数中有复杂的初始化需求或者对性能要求较高的情况下。
65.C的四种强制转换reinterpret_cast/const_cast/static_cast /dynamic_cast
C中的四种强制转换(static_cast、dynamic_cast、const_cast和reinterpret_cast)有以下区别 static_cast主要用于基本类型之间的转换、继承层次结构中的上行转换派生类指针转为基类指针、void指针和其他指针类型之间的转换等。在编译时完成类型检查具有一定的安全性。 dynamic_cast主要用于继承层次结构中的下行转换将基类指针或引用转换为派生类指针或引用。运行时进行类型检查如果转换失败会返回空指针对指针或抛出std::bad_cast异常对引用。 const_cast主要用于去除指针或引用的const或volatile属性实现指针或引用的类型转换用于修改类型的常量性属性。但要注意const_cast只能用于改变指针或引用的底层const性质不能用于改变被指对象的常量性。 reinterpret_cast主要用于指针之间的强制类型转换它会将一个指针类型转换为完全不同的指针类型即便是不同类型之间的转换都可以使用reinterpret_cast。它主要用于特殊的场景如指针和整数之间的转换或者不同种类指针之间的转换但使用时需要谨慎因为它执行的是低级操作可能会导致未定义行为。
需要注意的是任何类型之间的强制转换都可能带来潜在的风险因此在使用强制转换时需要谨慎并确保转换的安全性和正确性。
delete和delete[]的区别
delete和delete[]是在C中用于释放动态分配的内存的操作符它们有一些区别 delete用于释放使用new操作符分配的单个对象的内存。例如MyClass* obj new MyClass; delete obj;。它会调用对象的析构函数并释放对应的内存。 delete[]用于释放使用new[]操作符分配的数组对象的内存。例如int* arr new int[5]; delete[] arr;。它会调用数组中每个元素的析构函数如果有的话然后释放整个数组所占的内存空间。
需要注意的是使用delete释放使用new[]分配的数组对象的内存或使用delete[]释放使用new分配的单个对象的内存是不正确的这种行为会导致未定义的行为可能会造成内存泄漏或程序崩溃。
因此当你使用new分配单个对象时请使用delete释放内存当你使用new[]分配数组对象时请使用delete[]释放内存。这样可以确保正确释放内存并避免潜在的问题。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/82207.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!