CS504 — Final Term Summary (Lectures 23–42)
📘 Lecture 23 — Architectural Views, Styles, and Models
📖 Overview: This lecture introduces the concept of software architecture as a high-level structural blueprint composed of elements, forms, and rationale. It explains Krutchen's 4+1 and Clement's modified architectural view models, then covers major architectural styles including data-centered, client-server, layered, and pipes-and-filters architectures. The lecture also addresses partitioning and analysis of architectural designs, which are critical for satisfying both functional and non-functional requirements in large software systems.
🗂️ Topics Covered
The lecture covers architectural views including Krutchen's 4+1 model and Clement's modified version with Functional, Code, Development, Concurrency, Physical, and Scenarios views. It then discusses architectural styles: data-centered/repository model, client-server model with thin/fat/zero-install and n-tier configurations, data flow/pipes-and-filters architecture, layered architecture, and reference architectures like OSI. Finally, it covers horizontal and vertical partitioning and a step-by-step approach to analyzing architecture design.
📝 Lecture Summary
8.6 Architectural Views
Software architecture defines the high-level structure by organizing architectural elements. Perry and Wolfe proposed: Software architecture = {Elements, Forms, Rationale}. Elements are divided into three categories: data elements (information used/transformed), processing elements (transform data), and connecting elements (connect pieces together). Boehm modified this to: Software architecture = {Elements, Forms, Rationale/Constraints}.
Krutchen's 4+1 Architectural View Model proposes five views: the logical view (object model of design, for end-user functionality), the process view (concurrency and synchronization, for system integrators), the physical view (mapping software onto hardware, for system engineers), the development view (static organization in development environment, for programmers), and the use case view (scenarios to validate architecture).
💡 Why this matters: Using multiple views allows different stakeholders (end-users, programmers, integrators, system engineers) to analyze the architecture from their own perspective, ensuring all quality attributes are addressed.
Clement's Modified Version of Krutchen's 4+1 Model
Clements modified the model to include five views: Functional View, Concurrency View, Physical View, Development View, and Code View (an extension not in the original model), plus Scenarios.
The Functional View comprises functions, key system abstractions, and domain elements, connecting dependencies and data flows. Users include domain engineers, product-line engineers, and end users. It aids understanding functionality, modifiability, reusability, tool support, and work allocation.
The Development View documents files and directories in the system. Users include development staff, configuration management staff, and project managers. Uses include maintenance, testing, configuration management, and version control.
The Code View includes classes, objects, procedures, functions, subsystems, layers, and modules. It documents calling and containing hierarchies. Primary users are programmers and designers. Intent: maintenance and portability.
The Concurrency View documents parallel processes and threads, focusing on event synchronization and parallel data flows. Used by integrators, performance engineers, and testers. Purpose: identify opportunities for parallelism to improve performance.
The Physical View depicts physical organization and deployment including processors, sensors, and storage devices. Primary users: hardware and system engineers. Used for system delivery/installation, performance, availability, scalability, and security analysis.
Scenarios are use cases describing sequences of responsibilities and change cases. They validate that the architecture supports required functionality, communicate design, tie all views together, and help understand dynamic behavior and design limits.
| View | Components | Users | Rationale |
|---|---|---|---|
| Functional View | functions, key system abstractions, domain elements | domain engineers, product-line designers, end users | functionality, modifiability, product lines/reusability, tool support, work allocation |
| Code View | classes, objects, procedures, functions, subsystems, layers, modules | programmers, designers, reusers | modifiability/maintainability, portability, subsetability |
| Development View | files, directories | managers, programmers, configuration managers | managers, programmers, configuration managers |
| Physical View | CPUs, sensors, storage | hardware engineers, system engineers | system delivery/installation, performance, availability, scalability, security |
| Concurrency View | processes, threads | performance engineers, integrators, testers | performance, availability |
What Are Views Used For?
Views are an engineering tool to achieve desired system qualities. Each view provides an engineering handle on certain quality attributes. In small systems, distinct views may collapse (e.g., concurrency and physical views may be the same). Views are documentation vehicles for current and future development, used by managers and customers. Views must be annotated to support analysis using scenarios and design rationale.
Structures document how the current system was developed and how future development should occur. Each structure provides a method for reasoning about relevant quality attributes. The uses structure is engineered for extensibility, the calls structure for reducing bottlenecks, and the module structure for modifiability.
Hierarchical Views
Every view is potentially hierarchical:
- Functional view contains sub-functions
- Development view contains directories which contain files
- Code view has modules/systems containing sub-modules/sub-systems
- Concurrency view contains processes subdivided into threads
- Physical view clusters contain computers containing processors
Architectural views relate to each other in complicated ways. One must choose views useful to the system being built and to achieving important qualities. Views should be hierarchical where needed and contain enough annotated information to support desired analyses.
Architectural Styles
An architectural model may conform to a generic architectural style. Awareness of styles simplifies defining system architectures, though most large systems are heterogeneous and do not follow a single style.
Each style encompasses:
- A set of components (e.g., database, computational modules) performing functions
- A set of connectors enabling communication, coordination, and cooperation among components
- Constraints defining how components can be integrated
- Semantic models enabling understanding of overall system properties
8.7 Architectural Models
Like analysis models, different kinds of architectural models are developed during architectural design. Static structural models show major system components; dynamic process models show process structure; interface models define sub-system interfaces.
8.8 Architectural Styles
Architectural design may be based on patterns called architectural styles. Common styles include:
- Data-centered architectures
- Client Server Architecture and variations
- Layered architectures
- Reference Architecture
Data-Centered or Repository Model
Sub-systems exchange data in two ways:
- Shared data held in a central database or repository accessed by all sub-systems
- Each sub-system maintains its own database and passes data explicitly
When large amounts of data must be shared, the repository model is most commonly used, extensively in main-frame applications.
Advantages: Efficient way to share large amounts of data; sub-systems need not know how data is produced; centralized management (backup, security); provides global view of the system; sharing model published as repository schema.
Disadvantages: Sub-systems must agree on a repository data model (compromise); data evolution is difficult and expensive; little scope for specific management policies; difficult to distribute efficiently.
8.9 Client-Server Model
The client-server model distributes data and processing, shifting from main-frame applications where data management and processing occurred on the same computer. With cheaper, powerful machines, load shifted from back-end to smaller machines.
This distributed model shows how data and processing are distributed across components. Applications are modeled as services provided by servers and clients that use these services. The system is organized as stand-alone servers (providing printing, data management) and clients calling these services, connected through a network. Clients and servers are logical processes (not always physical machines). Clients know servers, but servers do not need to know all clients.
8.10 Client/Server Software Components
A typical client-server system is composed of:
- User interaction/presentation subsystem
- Application subsystem — implements requirements; may reside on client or server
- Database management subsystem
- Middleware — provides mechanisms and protocols to connect clients with servers
Representative Server Types:
- File servers — client requests records; server transmits records over network
- Database servers — client sends SQL queries; server processes and returns results
- Transaction servers — client invokes remote procedures on server; server executes and returns results
- Groupware servers — enable communication among clients using text, images, bulletin boards, video
Advantages: Effective use of networked systems; cheaper hardware; easier to add/upgrade servers; straightforward data distribution.
Disadvantages: No standard way of sharing data (sub-systems may use different data organization); data interchange may be inefficient; redundant management in each server; no central register of names/services.
8.11 Representative Client/Server Configurations
Thin Client Model: Initially used to migrate legacy systems to client-server. The legacy system acts as a server; GUI implemented on a client. Chief disadvantage: heavy processing load on both server and network.
Fat Client Model: More processing delegated to the client; application processing locally extended. Suitable for new systems where client capabilities are known in advance. More complex management (new versions installed on every client).
Zero Install: No installation on client side needed; no/little processing at client side. Trade-off between using client computing power versus maintenance overhead. Similar to thin-client, but network distributes server-side processing across multiple servers. Example: web-based applications — updates on web server reflected when users log in.
N-Tier Architecture: Enhances scalability and performance by distributing data and application using multiple server machines. May involve different server types: application server, web server, DB server.
Three-tier Architecture: Each architecture layer (presentation, application, database) runs on separate processors. Better performance than thin-client; simpler management than fat-client; highly scalable.
N-tier architecture generalizes three-tier to more than three layers, distributing different subsystems on different servers.
8.12 Data Flow or Pipes and Filters Architecture
Similar to data flow diagrams. Used when input data is processed through a series of transformations to yield output. Each processing step is a filter; the connecting link is a pipe through which information flows. Each filter works independently, requiring no knowledge of other filters.
If the dataflow has a single sequence with no alternative or parallel paths, it is called batch sequential.
Layered Architecture
A layered architecture has different layers (e.g., operating system). Each layer isolates the outer layer from inner complexities. The outer layer only needs to know the interface provided by the inner layer. If the inner layer changes but the interface stays the same, the outer layer is unaffected. This enhances portability.
Basic layers: User Interface Layer → Application Layer → Utility Layer → Core Layer.
8.13 Reference Architectures
A reference architecture is not physical; it is a reference for defining protocols and designing/implementing systems developed by different parties. Derived from studying the application domain, not existing systems. Used as basis for implementation or to compare systems. Acts as a standard for evaluation.
Example: OSI model — a layered model for communication systems. The success of the Internet (heterogeneous systems communicating) is evidence of this model's effectiveness.
8.14 Partitioning the Architecture
Partitioning distributes responsibilities to different subsystems for easier maintenance. Results in fewer side effects, easier testing and extension.
Horizontal partitioning defines separate branches of the module hierarchy for each major function; control modules coordinate communication between functions.
Vertical partitioning (factoring) divides the application from a decision-making perspective. Architecture is partitioned into horizontal layers with decision-making modules at the top and workers at the bottom.
8.15 Analyzing Architecture Design
Required characteristics may conflict. Trade-offs seek optimal combinations based on cost/benefit analysis. Analysis requires understanding what is required and establishing relative priority of attributes.
Steps for architectural analysis:
- Collect scenarios
- Elicit requirements, constraints, and environment description
- Describe architectural styles/patterns chosen (module view, process view, data flow view)
- Evaluate quality attributes by considering each attribute in isolation
- Identify sensitivity of quality attributes to various architectural attributes
- Critique candidate architectures using sensitivity analysis
⭐ Key Takeaways
The most critical concepts from this lecture are the different architectural view models (Krutchen's 4+1 and Clement's modified version) which allow multiple stakeholders to analyze the system from their perspectives, and the major architectural styles (repository, client-server with its variations, pipes-and-filters, layered, and reference architectures). Partitioning can be horizontal (by function) or vertical (by decision-making level), and architectural analysis follows a structured six-step process that collects scenarios, elicits requirements, describes styles, evaluates attributes, identifies sensitivities, and critiques candidates. The choice of architecture depends on the system's specific functional and non-functional requirements, and most large systems are heterogeneous, combining multiple styles. Finally, views must be hierarchical, annotated with scenarios and rationale, and chosen specifically for the quality attributes most important to the application.
🧠 Quick Revision Questions
-
What are the three categories of architectural elements according to Perry and Wolfe's formula, and what role does each play?
-
List the five views in Clement's modified version of Krutchen's 4+1 architectural view model, and identify the primary users and rationale for each view.
-
Compare the repository model and the client-server model in terms of data sharing, management, and scalability — what are the key advantages and disadvantages of each?
-
What is the difference between thin-client, fat-client, zero-install, and n-tier client-server configurations, and what problem does each solve?
-
What are the six steps for performing architectural analysis, and why is sensitivity analysis important in step 5?
📘 Lecture 26 — Introduction to Design Patterns
📖 Overview: This lecture introduces design patterns as reusable solutions to recurring software design problems. It covers the historical origins from Christopher Alexander’s architectural work, the formal GoF (Gang of Four) documentation format, and a classification of patterns into creational, structural, and behavioral categories. Three specific patterns (Observer, Singleton, and Façade) are examined in detail using the GoF template.
🗂️ Topics Covered
The lecture begins with Christopher Alexander’s definition of patterns and how they apply to object-oriented design. It defines design patterns formally, describes their documentation structure, and traces their history from the 1970s through the 1995 GoF book. The GoF format is explained with ten components. Patterns are classified into creational, structural, and behavioral types, with subclassification into class and object patterns. Three patterns are then detailed: Observer (behavioral), Singleton (creational), and Façade (structural).
📝 Lecture Summary
Design Patterns
Christopher Alexander says, “Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.” Even though Alexander was talking about patterns in buildings and towns, what he says is true about object-oriented design patterns. Our solutions are expressed in terms of objects and interfaces instead of walls and doors, but the core of both kinds of patterns is a solution to a problem in a context.
Design Patterns defined
“Description of communicating objects and classes that are customized to solve a general design in a particular context.” Patterns are devices that allow programs to share knowledge about their design. In our daily programming, we encounter many problems that have occurred, and will occur again. The question we must ask our self is how we are going to solve it this time. Documenting patterns is one way that you can reuse and possibly share the information that you have learned about how it is best to solve a specific program design problem. Essay writing is usually done in a fairly well defined form, and so is documenting design patterns. The general form for documenting patterns is to define items such as: the motivation or context that this pattern applies to; prerequisites that should be satisfied before deciding to use a pattern; a description of the program structure that the pattern will define; a list of the participants needed to complete a pattern; consequences of using the pattern (both positive and negative); and examples.
🔑 Definition — Design Pattern: A description of communicating objects and classes that are customized to solve a general design problem in a particular context.
Historical perspective of design patterns
The origin of design patterns lies in work done by an architect named Christopher Alexander during the late 1970s. He began by writing two books, A Pattern Language [Alex77] and A Timeless Way of Building [Alex79] which, in addition to giving examples, described his rationale for documenting patterns. The pattern movement became very quiet until 1987 when patterns appeared again at an OOPSLA conference. Since then, many papers and presentations have appeared, authored by people such as Grady Booch, Richard Helm, Erich Gamma, and Kent Beck. From then until 1995, many periodicals featured articles directly or indirectly relating to patterns. In 1995, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides published Design Patterns: Elements of Reusable Object-Oriented Software [Gamma95], which has been followed by more articles in trade journals.
The concept of design patterns is not new as we can find a number of similar pursuits in the history of program designing and writing. For instance, Standard Template Library (STL) is a library of reusable components provided by C++ compilers. Likewise, we use algorithms in data structures that implement typical operations of manipulating data in data structures. Another similar effort was from Peter Coad whose patterns are known for object-oriented analysis and design.
Anti-patterns is another concept that corresponds to common mistakes in analysis and design. These are identified in order to prevent potential design and analysis defects from entering into the design. Another similar concept is object-oriented framework that is a set of cooperative classes that make up reusable design of a system. Framework dictates the architecture of the software and describes the limitations and boundaries of architecture.
💡 Why this matters: Understanding the historical context shows that patterns are not new inventions but distilled wisdom from decades of software engineering practice.
GOF Design Pattern Format
The basic template includes ten things as described below:
Name: Works as an idiom; name has to be meaningful.
Problem: A statement of the problem which describes its intent; the goals and objectives it wants to reach within the given context.
Context: Preconditions under which the problem and its solutions seem to occur; result or consequence; state or configuration after the pattern has been applied.
Forces: Relevant forces and constraints and their interactions and conflicts; motivational scenario for the pattern.
Solution: Static and dynamic relationships describing how to realize the pattern; instructions on how to construct the work products; pictures, diagrams, prose which highlight the pattern’s structure, participants, and collaborations.
Examples: One or more sample applications to illustrate a specific context and how the pattern is applied.
Resulting context: The state or configuration after the pattern has been applied; consequences (good and bad) of applying the pattern.
Rationale: Justification of the steps or rules in the pattern; how and why it resolves the forces to achieve the desired goals, principles, and philosophies; how are the forces orchestrated to achieve harmony; how does the pattern actually work.
Related patterns: The static and dynamic relationships between this pattern and other patterns.
Known uses: To demonstrate that this is a proven solution to a recurring problem.
Classifications of patterns
Creational patterns: How to create and instantiate. Abstract the instantiation process and make the system independent of its creational process. Class creational rules use inheritance to vary the instantiated classes. Object creational rules delegate instantiation to another object. Examples: Abstract factory and factory method.
Structural patterns: Deals with object’s structure. Class structural patterns concern the aggregation of classes to form largest structures. Object structural patterns concern the aggregation of objects to form largest structures.
Behavioral patterns: Describe the patterns of communication between classes and objects; how objects are communicating with each other. Behavioral class patterns use inheritance to distribute behavior between classes. Behavioral object patterns use object composition to distribute behavior between classes. Help in distributing object’s intelligence; concern with algorithms and assignment of responsibilities between objects.
Observer Pattern
Name: Observer
Basic intent: It is intended to define a many to many relationship between objects so that when one object changes state all its dependants are notified and updated automatically. It provides a dependence/publish-subscribe mechanism in programming language. Smalltalk being the first pure Object Oriented language in which observer pattern was used in implementing its Model View Controller (MVC) pattern. It was a publish-subscribe mechanism in which views (GUIs) were linked with their models (containers of information) through controller objects. Therefore, whenever underlying data changes in the model objects, the controller would notify the view objects to refresh themselves and vice versa. MVC pattern was based on the observer pattern.
Motivation: It provides a common side effect of partitioning a system into a collection of cooperating classes that are in the need to maintain consistency between related objects.
Description: This can be used when multiple displays of state are needed.
Consequences: Optimizations to enhance display performance are impractical.
Many graphical user interface toolkits separate the presentational aspects of the user interface from the underlying application data. Classes defining application data and presentations can be reused independently. They can work together, too. Both a spreadsheet object and bar chart object can depict information in the same application data object using different presentations. The spreadsheet and the bar chart don’t know about each other, thereby letting you reuse only the one you need. But they behave as though they do. When the user changes the information in the spreadsheet, the bar chart reflects the changes immediately, and vice versa.
Structure:
- Subject: Provides interface for attaching/detaching Observer objects; knows its observers
- Observer: Defines updating interface for objects notified of subject changes
- ConcreteSubject: Stores state of interest to ConcreteObserver; sends notifications on state change
- ConcreteObserver: Maintains reference to ConcreteSubject; stores consistent state; implements Observer updating interface
Participants:
- Subject: Knows its observers. Any number of Observer objects may observe a subject. Provides an interface for attaching and detaching Observer objects.
- Observer: Defines an updating interface for objects that should be notified of changes in a subject.
- ConcreteSubject: Stores state of interest to ConcreteObserver objects. Sends a notification to its observers when its state changes.
- ConcreteObserver: Maintains a reference to a ConcreteSubject object. Stores state that should stay consistent with the subject’s. Implements the Observer updating interface to keep its state consistent with the subject’s.
Singleton Pattern
Intent: It ensures that a class only has one instance and provides a global point of access to it.
Applicability: Singleton pattern should be used when:
- There must be exactly one instance of a class and it must be accessible to clients from a well-known access point.
- Controlling the total number of instances that would be created for a particular class.
- The sole instance should be extensible by sub classing and clients should be able to use an extended instance without modifying their code.
Structure: Singleton defines an Instance operation that lets clients access its unique instance. Instance is a class operation (a class method in Smalltalk and a static member function in C++). May be responsible for creating its own unique instance.
Participants: Singleton defines an instance operation that lets clients access its unique instance. Instance is a class operation (that is, a class method in Smalltalk and a static member function in C++). May be responsible for creating its own unique instance.
Singleton Pattern Example: The Singleton class is declared as:
class Singleton {
public:
static Singleton* Instance();
protected:
Singleton();
private:
static Singleton* _instance;
};
The corresponding implementation is:
Singleton* Singleton::_instance = 0;
Singleton* Singleton::Instance() {
if (_instance == 0) {
_instance = new Singleton;
}
return _instance;
}
Clients access the singleton exclusively through the Instance member function. The variable _instance is initialized to 0, and the static member function Instance returns its value, initializing it with the unique instance if it is 0.
Façade Pattern
Intent: It provides a unified interface to a set of interfaces in a sub-system. Façade defines a higher level interface that makes a subsystem easier to use.
Applicability: You would use façade when:
- You want to provide a simple interface to a complex sub-system.
- There are many dependencies between clients and the implementation classes of an abstraction.
- You should introduce a façade to decouple the system from clients and other subsystems.
- You want to layer your subsystem.
Abstract example of façade: Structuring a system into subsystems helps reduce complexity. A common design goal is to minimize the communication and dependencies between subsystems. One way to achieve this goal is to introduce a façade object that provides a single, simplified interface to the more general facilities of a subsystem.
Structure: The Façade sits between client classes and subsystem classes, providing a single point of interaction.
Participants:
- Façade: Knows which subsystem classes are responsible for a request. Delegates client requests to appropriate subsystem objects.
- Subsystem classes: Implement subsystem functionality. Handle work assigned by the Façade object. Have no knowledge of the façade, that is, they keep no references to it.
⭐ Key Takeaways
Design patterns are proven, reusable solutions to recurring design problems, documented in a structured format (GoF) comprising name, problem, context, forces, solution, examples, resulting context, rationale, related patterns, and known uses. Patterns are classified into three categories: creational (how objects are created), structural (how objects/classes are composed), and behavioral (how objects communicate). The Observer pattern establishes a one-to-many dependency so that changes in one object automatically notify all dependents. The Singleton pattern ensures a class has exactly one instance with global access. The Façade pattern provides a simplified, unified interface to a complex subsystem, reducing dependencies between clients and subsystem classes.
🧠 Quick Revision Questions
- What are the ten components of the GoF design pattern format, and what does each describe?
- How do creational, structural, and behavioral patterns differ, and into what two subcategories is each divided?
- In the Observer pattern, what is the role of the Subject, and how does it interact with ConcreteObserver objects?
- In the Singleton pattern implementation, why is the _instance variable initialized to 0, and what happens during the first call to Instance()?
- How does the Façade pattern reduce complexity and dependencies between clients and a subsystem?
📘 Lecture 28 — Good Programming Practices and Guidelines
📖 Overview: This lecture focuses on how to write maintainable code that is easy for humans to understand. It covers the principles of self-documenting code, coding style guides, and comprehensive naming conventions for Java and C++. These practices are critical for producing software that can be efficiently maintained and enhanced over time.
🗂️ Topics Covered
This lecture covers the concept of maintainable code, emphasizing that code should be written for human readers, not just computers. It introduces the three guiding principles of maintainability: simplicity, clarity, and generality/flexibility. The concept of self-documenting code is explained as code that explains itself without needing external comments or documentation. The lecture details factors that contribute to self-documenting code, such as function size and identifier names. It then introduces a Coding Style Guide to ensure consistency across a project. Finally, it provides a comprehensive set of naming conventions for Java and C++, including Hungarian Notation, CamelCase, and specific rules for variables, functions, constants, and boolean flags.
📝 Lecture Summary
10.1 Maintainable Code
In most cases, maintainability is the most desirable quality of a software artifact. The three basic principles that guide maintainability are: simplicity, clarity, and generality or flexibility. Fowler states, "Any fool can write code that computers can understand, good programmers write code that humans can understand." The software will be easy to maintain if it is easy to understand and easy to enhance. 💡 Why this matters: Code is read far more often than it is written, especially during maintenance. Writing for human readers directly reduces long-term costs.
Self Documenting Code
A self documenting code is a code that explains itself without the need of comments and extraneous documentation. The meaning of the code should be evident just by reading the code.
Function Size The size of individual functions plays a significant role in understandability. As a function becomes longer, it becomes more difficult to understand. Ideally, a function should not be larger than 20 lines of code and in any case should not exceed one page in length. The idea is that the entire context of a function should be present on one screen or one printed page.
Identifier Names Identifier names play a significant role in enhancing readability. Names should be chosen to make them meaningful. Using meaningful names and named constants eliminates the need for explanatory comments, making the code self-documenting.
🔑 Definition — Self Documenting Code: Code that explains itself without the need for comments and extraneous documentation; its meaning should be evident just by reading the code. 📐 Rule: Keep functions to ≤ 20 lines (one screen) or ≤ one printed page. 📌 Example:
// Poor Coding Practice - requires comment
if (x==0) // this is the case when we are allocating a new number
// Improved - meaningful variable name
if (AllocFlag == 0)
// Self-Documenting - meaningful name and named constant
if (AllocFlag == NEW_NUMBER)
10.2 Coding Style Guide
Consistency plays a very important role in making code self-documenting. A coding style guide is aimed at improving the coding process and implementing standardized and relatively uniform code throughout the application or project. Since multiple programmers participate in developing a large piece of code, a consistent style must be adopted by all. Each organization should develop a style guide for its entire team.
10.3 Naming Conventions
Hungarian Notation is a variable naming convention that includes information about the variable in its name (such as data type, whether it is a reference variable or a constant). Bicapitalization or CamelCase is the practice of writing compound words where terms are joined without spaces, and every term is capitalized. The lecture's style guide uses a naming convention where Hungarian Notation is mixed with CamelCase.
🔑 Definition — Hungarian Notation: A variable naming convention that includes information about the variable (e.g., data type) in its name. 🔑 Definition — CamelCase (Bicapitalization): The practice of writing compound words where terms are joined without spaces, and every term is capitalized.
General Naming Conventions for Java and C++
- Types: Nouns, mixed case starting with upper case. (e.g.,
Line,FilePrefix) - Variables: Mixed case starting with lower case. (e.g.,
line,filePrefix). This distinguishes variables from types (e.g.,Line line;). - Constants: All uppercase with underscores separating words. (e.g.,
MAX_ITERATIONS,COLOR_RED). Use should be minimized; implementing the value as a method is often a better choice for readability and a uniform interface (e.g.,int getMaxIterations()). - Methods/Functions: Verbs, mixed case starting with lower case. (e.g.,
getName(),computeTotalWidth()) - Template Types (C++): A single uppercase letter. (e.g.,
template<class T> ...) - Global Variables (C++): Always referred to using the
::operator. (e.g.,::mainWindow.open()) - Private Class Variables: Should have a
_suffix to distinguish class scope from local scratch variables. (e.g.,private int length_;) - Abbreviations/Acronyms: Should not be uppercase when used as a name. (e.g.,
exportHtmlSource(), notexportHTMLSource()) - Generic Variables: Should have the same name as their type. (e.g.,
void setTopic(Topic topic))- Non-generic variables can be named by combining role and type (e.g.,
Point startingPoint).
- Non-generic variables can be named by combining role and type (e.g.,
- Language: All names should be written in English.
- Scope Length: Variables with a large scope should have long names; variables with a small scope can have short names.
- Object Implicit in Method Names: The name of the object is implicit and should be avoided in a method name. (e.g.,
line.getLength(), notline.getLineLength())
Specific Naming Conventions for Java and C++
- get/set: Used where an attribute is accessed directly (e.g.,
employee.getName(),matrix.setElement(2, 4, value)). - is prefix: Used for boolean variables and methods (e.g.,
isSet,isVisible). Alternatives includehas,can, andshouldprefixes (e.g.,boolean hasLicense()). - compute prefix: Used in methods where something is computed (e.g.,
matrix.computeInverse()). This signals a potentially time-consuming operation. - find prefix: Used in methods where something is looked up (e.g.,
vertex.findNearestVertex()). This signals a simple look-up with minimal computation. - initialize: Used where an object or concept is established (e.g.,
printer.initializeFontSet()). - List suffix: Used on names representing a list of objects (e.g.,
vertexList). Using the plural form should be avoided as it differs from the singular by only one character. - n prefix: Used for variables representing a number of objects (e.g.,
nPoints,nLines). - No suffix: For variables representing an entity number (e.g.,
tableNo,employeeNo). An alternative is to prefix them with ani(e.g.,iTable,iEmployee). - Iterator variables: Should be called
i,j,k, etc., following mathematical conventions. Variables namedj,kshould be used for nested loops only. - Complement names: Must be used for complement entities to reduce complexity by symmetry (e.g.,
get/set,add/remove,create/destroy,open/close,show/hide). - Abbreviations: Abbreviations in names should be avoided (e.g., use
computeAverage(), notcompAvg()). Domain-specific phrases that are known by their acronym should be kept abbreviated (e.g.,html, notHypertextMarkupLanguage). - Negated boolean names: Must be avoided to prevent double negatives. (e.g., use
boolean isError;, notboolean isNotError;) - Functions vs. Procedures: Functions (methods returning an object) should be named after what they return; procedures (void methods) should be named after what they do.
⭐ Key Takeaways
The most critical concept from this lecture is that maintainability is a primary quality goal, achieved by writing self-documenting code that is simple, clear, and flexible. The most powerful tool for this is choosing meaningful names for identifiers (variables, functions, constants) and using named constants instead of magic numbers, which makes code explain itself without comments. Consistency enforced by a coding style guide is essential for team projects, ensuring uniform code that is easy for any team member to read and maintain. You must remember the rules for function size (≤20 lines/one page) and the specific naming conventions for Java and C++, such as using get/set prefixes for accessors, is/has/can prefixes for booleans, and differentiating between types (PascalCase), variables (camelCase), and constants (UPPER_CASE). Finally, avoid abbreviations, negated boolean names, and ensure methods are named after what they return or do.
🧠 Quick Revision Questions
- What are the three basic principles that guide maintainability in code?
- What is the recommended maximum size for a function in lines of code, and why?
- In Java/C++ naming conventions, what naming style (e.g., PascalCase, camelCase, UPPER_CASE) is used for: a) types, b) variables, c) constants?
- Which specific prefixes should be used for boolean methods and variables, and why should negated boolean names (like
isNotError) be avoided? - What is the purpose of using a coding style guide in a software project?
📘 Lecture 29 — File handling tips for Java and C++
📖 Overview: This lecture covers important file handling conventions and coding standards for Java and C++. It establishes guidelines for file organization, include statements, class declarations, and statement formatting, which are crucial for maintaining readable, consistent, and error-free code in multi-programmer environments.
🗂️ Topics Covered
File handling tips for Java and C++ including header file extensions, class declaration conventions, and include file constructions. Organization of class and interface declarations with proper ordering of variables, constructors, and methods. Comprehensive guidelines for statements including type conversions, variable declarations, loop structures, conditionals, and miscellaneous coding practices like avoiding magic numbers and using explicit type conversions.
📝 Lecture Summary
File handling tips for Java and C++
C++ header files should have the extension .h. Source files can have the extension .c++ (recommended), .C, .cc or .cpp. Example: MyClass.c++, MyClass.h. These are all accepted C++ standards for file extension.
Classes should be declared in individual header files with the file name matching the class name. Secondary private classes can be declared as inner classes and reside in the file of the class they belong to. All definitions should reside in source files. The header files should declare an interface, the source file should implement it. When looking for an implementation, the programmer should always know that it is found in the source file. The obvious exception to this rule is inline functions that must be defined in the header file.
Special characters like TAB and page break must be avoided. These characters are bound to cause problems for editors, printers, terminal emulators or debuggers when used in a multi-programmer, multi-platform environment.
🔑 Definition — Inline functions: Functions that must be defined in the header file, as they are the exception to the rule that definitions should reside in source files.
Include Files and Include Statements for Java and C++
Header files must include a construction that prevents multiple inclusion. The convention is an all uppercase construction of the module name, the file name and the h suffix.
#ifndef MOD_FILENAME_H
#define MOD_FILENAME_H
:
#endif
The construction is to avoid compilation errors. The construction should appear in the top of the file (before the file header) so file parsing is aborted immediately and compilation time is reduced.
Classes and Interfaces
Class and Interface declarations should be organized in the following manner:
- Class/Interface documentation.
classorinterfacestatement.- Class (static) variables in the order public, protected, package (no access modifier), private.
- Instance variables in the order public, protected, package (no access modifier), private.
- Constructors.
- Methods (no specific order).
Statements in Java and C++
Types
Type conversions must always be done explicitly. Never rely on implicit type conversion.
floatValue = (float) intValue; // NOT: floatValue = intValue;
By this, the programmer indicates that he is aware of the different types involved and that the mix is intentional.
Types that are local to one file only can be declared inside that file.
The parts of a class must be sorted public, protected and private. All sections must be identified explicitly. Not applicable sections should be left out. The ordering is "most public first" so people who only wish to use the class can stop reading when they reach the protected/private sections.
💡 Why this matters: This ordering puts the most relevant information first for users of the class, while implementers can read further to understand the protected and private details.
Variables
Variables should be initialized where they are declared and they should be declared in the smallest scope possible.
Variables must never have dual meaning. This enhances readability by ensuring all concepts are represented uniquely. This reduces chance of error by side effects.
Class variables should never be declared public. The concept of information hiding and encapsulation is violated by public variables. Use private variables and access functions instead. One exception to this rule is when the class is essentially a data structure, with no behavior (equivalent to a C++ struct). In this case it is appropriate to make the class' instance variables public.
Related variables of the same type can be declared in a common statement. Unrelated variables should not be declared in the same statement.
float x, y, z;
float revenueJanuary, revenueFebrury, revenueMarch;
The common requirement of having declarations on separate lines is not useful in the situations like the ones above. It enhances readability to group variables.
Variables should be kept alive for as short a time as possible. Keeping the operations on a variable within a small scope makes it easier to control the effects and side effects of the variable.
Global variables should not be used. Variables should be declared only within scope of their use. Same is recommended for global functions or file scope variables. It is easier to control the effects and side effects of the variables if used in limited scope.
Implicit test for 0 should not be used other than for boolean variables and pointers.
if (nLines != 0) // NOT: if (nLines)
if (value != 0.0) // NOT: if (value)
It is not necessarily defined by the compiler that ints and floats 0 are implemented as binary 0. Also, by using explicit test the statement gives immediate clue of the type being tested. It is common also to suggest that pointers shouldn't test implicit for 0 either, i.e. if (line == 0) instead of if (line). The latter is regarded as such a common practice in C/C++ however that it can be used.
Loop structures
Only loop control statements must be included in the for() construction.
sum = 0; // NOT: for (i=0, sum=0; i<100; i++)
for (i=0; i<100; i++) // sum += value[i];
sum += value[i];
Loop variables should be initialized immediately before the loop.
boolean done = false; // NOT: boolean done = false;
while (!done) { // :
: // while (!done) {
} // :
The use of do .... while loops should be avoided. There are two reasons for this. First, the construct is superfluous; any statement that can be written as a do .... while loop can equally well be written as a while loop or a for loop. Complexity is reduced by minimizing the number of constructs being used. The other reason is readability. A loop with the conditional part at the end is more difficult to read than one with the conditional at the top.
The use of break and continue in loops should be avoided. These statements should only be used if they prove to give higher readability than their structured counterparts. In general, break should only be used in case statements and continue should be avoided altogether.
The form for (;;) should be used for empty loops.
for (;;) { // NOT: while (true) {
: // :
} // }
This form is better than the functionally equivalent while (true) since this implies a test against true, which is neither necessary nor meaningful. The form while(true) should be used for infinite loops.
Conditionals
Complex conditional expressions must be avoided. Introduce temporary boolean variables instead.
if ((elementNo < 0) || (elementNo > maxElement)||
elementNo == lastElement) {
:
}
Should be replaced by:
boolean isFinished = (elementNo < 0) || (elementNo > maxElement);
boolean isRepeatedEntry = elementNo == lastElement;
if (isFinished || isRepeatedEntry) {
:
}
The nominal case should be put in the if-part and the exception in the else-part of an if statement.
boolean isError = readFile (fileName);
if (!isError) {
:
}
else {
:
}
The conditional should be put on a separate line.
if (isDone) // NOT: if (isDone) doCleanup();
doCleanup();
Executable statements in conditionals must be avoided.
file = openFile (fileName, "w"); // NOT: if ((file = openFile(fileName, "w")) != null) {
if (file != null) { // :
: // }
}
Miscellaneous
The use of magic numbers in the code should be avoided. Numbers other than 0 and 1 should be considered declared as named constants instead.
Floating point constants should always be written with decimal point and at least one decimal.
double total = 0.0; // NOT: double total = 0;
double speed = 3.0e8; // NOT: double speed = 3e8;
double sum;
:
sum = (a + b) * 10.0;
This emphasizes the different nature of integer and floating point numbers even if their values might happen to be the same in a specific case. Also, as in the last example above, it emphasizes the type of the assigned variable (sum) at a point in the code where this might not be evident.
Floating point constants should always be written with a digit before the decimal point.
double total = 0.5; // NOT: double total = .5;
The number and expression system in Java is borrowed from mathematics and one should adhere to mathematical conventions for syntax wherever possible. Also, 0.5 is a lot more readable than .5; there is no way it can be mixed with the integer 5.
Functions in C++ must always have the return value explicitly listed.
int getValue() // NOT: getValue()
{
:
}
If not explicitly listed, C++ implies int return value for functions.
goto in C++ should not be used. Goto statements violate the idea of structured code. Only in some very few cases (for instance breaking out of deeply nested structures) should goto be considered, and only if the alternative structured counterpart is proven to be less readable.
⭐ Key Takeaways
The lecture emphasizes consistent file organization and coding standards across Java and C++. Critical rules include: header files must have multiple inclusion guards using #ifndef constructs; classes should be declared in individual header files with matching names; type conversions must be explicit to avoid ambiguity; variables should be declared in the smallest possible scope and initialized immediately; complex conditionals should be replaced with temporary boolean variables for readability; magic numbers must be avoided in favor of named constants; loop constructs like do...while, break, continue should be minimized; and global variables should never be used to maintain encapsulation. These practices ensure code is maintainable, readable, and minimizes errors in multi-programmer environments.
🧠 Quick Revision Questions
- What is the purpose of the
#ifndef/#define/#endifconstruction in C++ header files, and where should it be placed in the file? - In what order should class/interface variables and constructors be declared, and why is "most public first" the recommended ordering?
- Why should complex conditional expressions be avoided, and what is the recommended alternative using a code example?
- What is the rule about implicit testing for 0, and which types are exceptions to this rule?
- Why should
do...whileloops andgotostatements be avoided in C++ code?
📘 Lecture No. 30 — Layout and Comments in Java and C++
📖 Overview: This lecture addresses code readability and maintainability by establishing guidelines for comments, expressions, statements, and avoiding cryptic code. It emphasizes making code self-documenting through proper formatting and clear logic rather than relying on comments or clever shortcuts.
🗂️ Topics Covered
The lecture covers commenting guidelines for Java and C++, including the problem with comments and proper indentation. It then addresses expression and statement layout, including natural form for expressions, parenthesization, and breaking down complex expressions. Finally, it warns against shortcuts and cryptic code, providing examples of common pitfalls and better alternatives.
📝 Lecture Summary
10.6 Layout and Comments in Java and C++
Comments present a fundamental problem: they lie. Comments are not syntax checked, and there is nothing forcing them to be accurate. As code undergoes change during schedule crunches, comments become less and less accurate.
As Fowler puts it, comments should not be used as deodorants. Tricky code should not be commented but rewritten. In general, the use of comments should be minimized by making the code self-documenting through appropriate name choices and an explicit logical structure.
If comments are necessary, these guidelines should be observed:
- All comments should be written in English (preferred in international environments)
- Use // for all comments, including multi-line comments. Since multilevel commenting is not supported in C++ and Java, using // ensures it is always possible to comment out entire sections using /* */ for debugging.
- Comments should be indented relative to their position in the code
🔑 Definition — Self-documenting code: Code that is written clearly enough through appropriate naming and logical structure that it does not need comments to be understood.
📌 Example of proper comment indentation:
// CORRECT:
while (true) {
// Do something
something();
}
// NOT:
while (true) {
// Do something
//
// Do something
something();
}
10.7 Expressions and Statements Layout
Basic indentation should be 2 spaces. Indentation of 1 is too small to emphasize the logical layout. Indentation larger than 4 makes deeply nested code difficult to read and increases the chance that lines must be split. 2 is chosen to reduce the chance of splitting code lines.
📐 Formula: Indentation = 2 spaces → optimal balance between readability and avoiding line splitting
Natural form for expressions: Expressions should be written as if they were spoken out aloud. Conditional expressions with negation are always difficult to understand.
📌 Example of rewriting with natural form:
// PROBLEMATIC:
if (! (block < activeBlock) || !(blockId >= unblocks))
// BETTER:
if ((block >= activeBlock) || (blockId < unblocks))
Parenthesize to remove ambiguity: Parentheses should always be used as they reduce complexity and clarify grouping. This is especially important when different unrelated operators are used in the same expression, as precedence rules are often assumed incorrectly by programmers.
📌 Example of precedence error:
// PROBLEMATIC: == has higher precedence than &
if (x & MASK == BITS)
// This compares MASK and BITS first, then ANDs result with x
// CORRECT with parentheses:
if ((x & MASK) == BITS)
📌 Example with leap year calculation:
// AMBIGUOUS:
leapYear = year % 4 == 0 && year % 100 != 0 || year % 400 == 0 ;
// SELF-EXPLANATORY:
leapYear = ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
Break up complex expressions: An expression is considered complex if it uses many operators in a single statement. Complex expressions should be broken down into multiple statements.
📌 Example of breaking down:
// PROBLEMATIC - too complex:
*x += (*xp=(2*k < (n-m) ? c[k+1] : d[k--]));
// BETTER - broken down:
if (2*k < n-m)
*xp = c[k+1];
else
*xp = d[k--];
*x = *x + *xp;
10.8 Shortcuts and Cryptic Code
Programmers sometimes write very concise code using shortcuts and tricks, resulting in cryptic code that is difficult to follow. Maintenance of such code becomes a nightmare.
Example 1 - Operator shortcuts:
// PROBLEMATIC - ambiguous semantics:
x *= a + b; // Is this x = x*a+b or x = x*(a+b)?
// The second is correct but not obvious from syntax
Example 2 - Complex shift operations:
// PROBLEMATIC - cryptic:
subkey = subkey >> (bitoff – (bitoff >> 3) << 3));
// This masks bitoff with octal 7
// BETTER:
subkey = subkey >> (bitoff & 0x7);
Example 3 - Hidden semantics with shifts:
// PROBLEMATIC - intent hidden:
a = a >> 2;
// Technical means divide by 4, but intent is unclear
// BETTER - explicit intent:
a = a/4;
Example 4 - Circular queue implementation:
// PROBLEMATIC - using % operator obscures logic:
bool Queue::add(int n) {
int k = (rear+1) % MAX_SIZE;
if (front == k) return false;
else {
rear = k;
queue[rear] = n;
return true;
}
}
// BETTER - explicit logic:
bool Queue::add() {
if (! isFull() ) {
rear++;
if (rear == MAX_SIZE) rear = 0;
QueueArray[rear] = n;
size++;
return true;
}
else return false;
}
bool Queue::isFull(int n) {
if (size == MAX_SIZE) return true;
else return false;
}
💡 Why this matters: The explicit logic version made it easy for students to implement double-ended queues correctly, while the cryptic version using % caused almost everyone to make mistakes with operations like rear = (rear-1) % MAX_SIZE.
⭐ Key Takeaways
Comments lie and should be minimized; instead, make code self-documenting through proper naming and logical structure. Always use // for comments (not /* */), indent them properly, and write in English. Use 2-space indentation for blocks, write expressions in natural spoken form, always parenthesize to clarify operator precedence, and break complex expressions into multiple statements. Avoid cryptic shortcuts like using bit shifts for division (use a/4 instead of a>>2) or tricky modulo operations for circular structures; state logic explicitly and use counting (size) instead of tricky pointer comparisons.
🧠 Quick Revision Questions
-
Why does Fowler say "comments should not be used as deodorants"? What should be done instead of commenting tricky code?
-
What is the recommended indentation size and why was this specific size chosen?
-
In the leap year example, why does adding parentheses make the code self-documenting when the logic hasn't actually changed?
-
Convert the following cryptic statement into clearer code and explain why:
a = a >> 2; -
What specific problem arose when students attempted to implement a double-ended queue using the
%operator approach, and how did the explicit counting approach solve it?
📘 Lecture 31 — Coding Style Guidelines (Continued)
📖 Overview: This lecture continues the discussion of coding style guidelines, focusing on problematic programming practices that reduce code readability and maintainability. It covers proper switch statement construction, the dangers of magic numbers, and appropriate use of the number zero in C/C++ programs, emphasizing the importance of self-documenting code.
🗂️ Topics Covered
The lecture examines three specific coding style issues: proper switch statement formatting with explicit break statements to avoid confusing fall-through behavior, the problem of magic numbers (unnamed constants) and how to replace them with named enumerated constants, and the misuse of the number zero for multiple different purposes in C/C++ code. Each topic includes concrete examples showing both poor and improved coding practices.
📝 Lecture Summary
Switch Statement
Switch statements should always end each case with a break statement. A tricky sequence of fall-through code like the one below causes more trouble than being helpful.
switch(c) {
case '-': sign = -1;
case '+': c = getchar();
case '.': break;
default: if (! isdigit(c))
return 0;
}
This code is cryptic and difficult to read. It is much better to explicitly write what is happening, even at the cost of duplication.
switch(c) {
case '-': sign = -1;
c = getchar();
break;
case '+': c = getchar();
break;
case '.': break;
default: if (! isdigit(c))
return 0;
break;
}
It would even be better if such code is written using the if statement as shown below.
if (c == '-') {
sign = -1;
c = getchar();
}
else if (c == '+') {
c = getchar();
}
else if (c != '.' && !isdigit(c)) {
return 0;
}
🔑 Definition — Fall-through: The behavior in a switch statement where execution continues from one case to the next without a break statement, which is often confusing and should be avoided in most situations.
Magic Numbers
Consider the following code segment:
fac = lim / 20;
if (fac < 1)
fac = 1;
for (i =0, col = 0; i < 27; i++, j++) {
col += 3;
k = 21 – (let[i] /fac);
star = (let[i] == 0) ? ' ' : '*';
for (j = k; j < 22; j++)
draw(j, col, star);
}
draw(23, 1, ' ');
for (i='A'; i <= 'Z'; i++)
cout << i;
Can you tell by reading the code what is meant by the numbers 20, 27, 3, 21, 22, and 23? These are constants that mean something but they do not give any indication of their importance or derivation, making the program hard to understand and modify. To a reader they work like magic and hence are called magic numbers. Any number (even 0 or 1) used in the code is a magic number. It should rather have a name of its own that can be used in the program instead of the number.
The difference would be evident if we look at the code segment below that achieves the same purpose:
enum {
MINROW = 1,
MINCOL = 1,
MAXROW = 24,
MAXCOL = 80,
LABELROW = 1,
NLET = 26,
HEIGHT = MAXROW –4,
WIDTH = (MAXCOL-1) / NLET
};
fac = (lim+HEIGHT-1) /HEIGHT;
if (fac < 1)
fac = 1;
for (i =0; i < NLET; i++) {
if (let[i] == 0)
continue;
for (j = HEIGHT – let[i] / fac; j < HEIGHT; j++)
draw(j-1 + LABELROW, (i+1)*WIDTH, '*');
}
draw(MAXROW-1, MINCOL+1, ' ');
for (i='A'; i <= 'Z'; i++)
cout << i;
🔑 Definition — Magic numbers: Any numeric constant (including 0 or 1) appearing directly in code without a named symbolic constant to explain its meaning, making the code difficult to understand and modify.
📌 Example: In the first code, 27 represents the number of letters in the alphabet (which should be NLET = 26), 20 represents a height calculation (HEIGHT = MAXROW – 4 = 20), and 23 represents the maximum row (MAXROW – 1 = 23). The improved version replaces all these with meaningful names.
💡 Why this matters: Magic numbers create maintenance nightmares — if a value changes, you must find and update every occurrence manually, and there's no indication of which numbers are related.
Use (or abuse) of Zero
The number 0 is the most abused symbol in programs written in C or C++. One can easily find code segments that use 0 in a fashion similar to the examples below in almost every C/C++ program.
flag = 0; // flag is boolean
str = 0; // str is string
name[i] = 0; // name is char array
x = 0; // x is floating pt
i = 0; // i is integer
This is a legacy of old style C programming. It is much better to use symbols to explicitly indicate the intent of the statement. It is easy to see that the following code is more in line with the self-documentation philosophy than the code above.
flag = false;
str = NULL;
name[i] = '\0';
x = 0.0;
i = 0;
🔑 Definition — Self-documenting code: Code that uses meaningful symbolic constants and type-appropriate values instead of generic numbers, making the programmer's intent clear without requiring comments.
📌 Example: str = 0 is ambiguous — does it mean the string is empty, or the pointer is null? Using str = NULL explicitly indicates a null pointer, while str = "" would indicate an empty string.
💡 Why this matters: Using 0 for multiple purposes (boolean false, null pointer, null character, floating point zero, integer zero) confuses readers and can hide bugs, especially in languages with weak type checking.
⭐ Key Takeaways
Switch statements must always include explicit break statements at the end of each case to prevent confusing fall-through behavior; consider using if-else statements when the switch logic becomes complex. Magic numbers — any unnamed constant in code — must be replaced with named symbolic constants or enumerations to make code understandable and maintainable. The number zero should never be used as a generic placeholder for different data types; instead, use false for booleans, NULL for pointers, '\0' for null characters, and 0.0 for floating-point values. Self-documenting code is achieved by choosing the right symbolic constant for each context, which eliminates the need for explanatory comments about what values mean. These three practices directly improve code clarity, reduce bugs during maintenance, and make programs easier for other developers to understand.
🧠 Quick Revision Questions
- Why is fall-through behavior in switch statements considered problematic, and what is the recommended alternative?
- What is a magic number, and how do you fix code that contains magic numbers?
- In the magic numbers example, what did the number
27represent, and why was this incorrect? - What are four different symbolic constants that should replace the number
0in boolean, pointer, character, and floating-point contexts? - What is the principle of self-documenting code, and how does it apply to the three topics covered in this lecture?
📘 Lecture 32 — Clarity through Modularity
📖 Overview: This lecture explores how breaking code into smaller, modular functions improves readability and maintainability. It also covers important C/C++ programming pitfalls including short-circuiting of logical operators and operand evaluation order with side effects—critical concepts for writing reliable, bug-free software.
🗂️ Topics Covered
The lecture covers three main topics: using modularity to achieve clarity in code through function decomposition, demonstrated with selection sort and quick sort examples; short-circuiting behavior of logical operators && and || with practical examples of guard conditions; and operand evaluation order combined with side effects, showing how unspecified function evaluation order leads to unpredictable results when functions modify parameters or global state.
📝 Lecture Summary
Clarity through modularity
Abstraction and encapsulation help manage program complexity. Breaking large functions into smaller ones improves readability. The selection sort example shows how a single function can be decomposed into swap and minimum functions, making the code shorter and more readable. As a byproduct, these smaller functions become reusable. Reusability is important but modularity is equally critical—functions should be broken into smaller pieces even if those pieces are not reused elsewhere.
🔑 Definition — Modularity: The practice of breaking a program into smaller, self-contained functions or modules to improve readability, maintainability, and reusability.
📌 Example: Original selectionSort function (single function with nested loops) was decomposed into:
void swap(int &x, int &y)— swaps two valuesint minimum(int a[], int from, int to)— finds minimum element index in a rangevoid selectionSort(int a[], int size)— now clear, calls minimum() and swap()
The quickSort algorithm demonstrates the same principle. The original code (approximately 25 lines) is difficult to remember. After decomposition into partition and recursive calls, the algorithm becomes trivial:
void quickSort(int a[], int left, int right) {
int p;
if (left < right) {
p = partition(a, left, right);
quickSort(a, left, p-1);
quickSort(a, p+1, right);
}
}
💡 Why this matters: Modular decomposition transforms complex algorithms into understandable, maintainable code that can be independently tested and reused.
Short circuiting || and &&
The logical operators && (AND) and || (OR) in C/C++ follow the short circuiting rule: expressions are evaluated left to right, and evaluation stops as soon as the final truth value can be determined. Short-circuiting allows one boolean expression to "guard" a potentially unsafe operation in a second expression, and saves time in complex expression evaluation.
🔑 Definition — Short circuiting: A property of logical operators where evaluation stops as soon as the overall result is known, without evaluating remaining operands.
📌 Example: A commercial banking software had this incorrect code:
while (ptr->data < myData && ptr != NULL) { // WRONG order
The guard ptr != NULL was placed second, but due to short-circuiting, the first expression ptr->data is evaluated first. If ptr is NULL, accessing ptr->data causes a crash. The corrected version:
while (ptr != NULL && ptr->data < myData) { // CORRECT order
Now if ptr is NULL, the first condition fails and the second is never evaluated.
💡 Why this matters: Always place guard conditions (like null checks) first before the potentially unsafe operations they protect.
Operand Evaluation Order and Side Effects
A side effect occurs when a function, besides returning a value, changes either one of its parameters or a variable declared outside the function. Side effects are a major source of programming errors. Many languages (including C/C++) do not specify function evaluation order in a single statement, which combined with side effects causes major problems.
🔑 Definition — Side effect: Any change to a parameter or external variable made by a function in addition to returning its explicit result.
📌 Example 1: With different parameters
c = f1(a) + f2(b);
// Functions:
int f1(int &x) { x = x * 2; return x + 1; }
int f2(int &y) { y = y / 2; return y - 1; }
// Initial: a=3, b=4
// Result: a=6, b=2, c=8
This works fine because different variables are affected.
📌 Example 2: With the same variable (problematic)
c = f1(a) + f2(a);
// Initial: a=3
If f1 evaluated first: a becomes 6, f1 returns 7, then f2 uses a=6, changes to 3, returns 2 → c=9, a=6 If f2 evaluated first: a becomes 1, f2 returns 0, then f1 uses a=1, changes to 2, returns 3 → c=3, a=2 Different order → completely different results!
💡 Why this matters: Never write code that depends on evaluation order when functions have side effects, as different compilers or platforms may evaluate in different orders.
⭐ Key Takeaways
Modularity dramatically improves code readability and should be applied even when functions are not reused—the selection sort and quick sort examples demonstrate this principle. When using short-circuiting operators && and ||, always place guard conditions (like null checks) first to prevent unsafe operations from being executed. Side effects in functions create dangerous dependencies on evaluation order, which is unspecified in C/C++—avoid relying on evaluation order when functions modify parameters or global state. Functions with side effects should be redesigned to separate computation from state modification. Always prefer pure functions (no side effects) for clarity and reliability.
🧠 Quick Revision Questions
- What are the two benefits of breaking the selectionSort function into smaller functions (swap and minimum)?
- In the quickSort example, what does the partition function do and why does this improve readability?
- What is wrong with
while (ptr->data < myData && ptr != NULL)and how should it be corrected? - Why does
c = f1(a) + f2(a)produce different results depending on evaluation order? - What is a side effect and why is it dangerous in C/C++ when combined with unspecified evaluation order?
📘 Lecture 33 — Common Coding Mistakes
📖 Overview: This lecture addresses two critical aspects of software engineering: common coding mistakes caused by side effects and performance optimization strategies. Understanding side effects helps programmers avoid subtle, hard-to-debug errors, while performance profiling teaches how to efficiently optimize code by focusing on bottlenecks rather than premature optimization.
🗂️ Topics Covered
The lecture covers common coding mistakes resulting from side effects in expressions involving array indices and assignments, along with guidelines to avoid these hazards. It then transitions to performance considerations, explaining the 80/20 rule, profiling techniques, and strategies for optimizing bottlenecks by either using better algorithms or rewriting problematic code sections, with detailed examples from an isspam function case study.
📝 Lecture Summary
Common Coding mistakes
Following is a short list of common mistakes made due to side effects — where an expression modifies a variable and also uses that same variable, causing unpredictable results.
-
array[i++] = i;— If i is initially 3, the array element might be set to 3 or 4, depending on the order of evaluation. -
array[i++] = array[i++] = x;— Due to side effects, multiple assignments become very dangerous. The result depends entirely on when i is incremented. -
The comma operator (
,) is very dangerous as it causes side effects. Consider:int i, j = 0;— Many people assume i is also initialized to 0, but it is not. Similarly,a = b, c = 0;— A majority of programmers would assume all a, b, and c are being initialized to 0, while only c is initialized and a and b have garbage values. This kind of overlook causes major programming errors that are not caught easily.
💡 Why this matters: Side effects in expressions create dependencies on evaluation order, which varies across compilers and can produce different results each time the code is executed.
🔑 Definition — Side Effect: A side effect occurs when an expression modifies the value of a variable while also using that variable elsewhere in the same expression, leading to unpredictable behavior based on evaluation order.
📐 Guidelines to avoid side effects:
- Never use
,except for declarations - If you are initializing a variable at declaration time, do not declare another variable in the same statement
- Never use multiple assignments in the same statement
- Be very careful when you use functions with side effects — functions that change the values of the parameters
- Try to avoid functions that change the value of some parameters and return some value at the same time
Performance
In many cases, performance and maintainability are at odds with one another. When planning for performance, one should always remember the 80/20 rule — you spend 80 percent of your time in 20 percent of the code. That is, we should not try to optimize everything. The proper approach is to profile the program and then identify bottlenecks to be optimized. This is similar to database normalization — we normalize to remove redundancies but then partially de-normalize if there are performance issues.
🔑 Definition — Profiling: The process of measuring where a program spends its execution time to identify performance bottlenecks (the "hot spots").
As an example, consider the isspam function profiled by calling it 10000 times. The results showed:
- strchr: 113 seconds, 44% of time, 900009440 calls
- strncmp: 15 seconds, 28% of time, 156646000 calls
- strstr: 6 seconds, 8% of time, 854500000 calls
- strlen: 4 seconds, 56% of time, 22555934 calls
- isspam: 21 seconds, 95% of time, 28510000 calls
- 11 other functions had insignificant performance overhead
The profiling revealed that most time was spent in strchr and strncmp, and both were called from strstr.
When a small set of functions that use each other is overwhelmingly the bottleneck, there are two alternatives:
- Use a better algorithm
- Rewrite the whole set
In this case, strstr was rewritten and profiled again. Although it was much faster, now 99.8% of the time was spent in strstr. The algorithm was rewritten again by eliminating strstr, strchr, and strncmp and using memcmp. While memcmp was much more complex than strstr, it gained efficiency by eliminating a number of loops. The new results showed:
- memcmp: 880 seconds, 88% of time, 1027590000 calls
- isspam: 66 seconds, 55% of time, 902920000 calls
- strlen: only 140 seconds, 3041060 calls (down from over 2 million)
The trick is to concentrate on hot spots by first identifying them and then cooling them. As mentioned, most time is spent in loops, so loops need to be the focus.
Consider this example:
for (j = i; j < MAX_FIELD; j++)
clear(j);
This loop clears fields before each new input is read. It was taking almost 50% of total time. On investigation, MAX_FIELD was 200 but actual fields needing clearing were 2 or 3 in most cases. The code was modified to:
for (j = i; j < maxField; j++)
clear(j);
This reduced overall execution time by half.
📌 Example: The MAX_FIELD loop optimization — by replacing the constant 200 with a variable that tracked actual fields needing clearing (typically 2-3), execution time was halved from ~50% of total runtime to ~25%.
⭐ Key Takeaways
The two most critical lessons from this lecture are: first, side effects in expressions like array[i++] = i or multiple assignments create unpredictable behavior that depends on compiler evaluation order — the guidelines provide a simple way to avoid these errors entirely. Second, performance optimization should follow the 80/20 rule: never optimize prematurely, instead profile to find the 20% of code consuming 80% of time, focus on hot spots (especially loops), and be willing to rewrite entire function sets or replace algorithms entirely rather than making small tweaks. The isspam case study demonstrates how replacing three string functions with one (memcmp) eliminated multiple loops and dramatically improved performance.
🧠 Quick Revision Questions
- What makes the statement
array[i++] = idangerous, and why might different compilers give different results? - What are the five guidelines given to avoid side-effect-related coding errors?
- What is the 80/20 rule in the context of performance optimization, and why should we not optimize everything?
- In the isspam profiling example, what was the final optimization strategy and why did it work better than just rewriting strstr?
- How did changing
MAX_FIELDtomaxFieldin the loop example reduce execution time by half?
📘 Lecture 34 — Portability
📖 Overview: This lecture addresses the critical challenge of making software portable across different platforms. It explains why portability matters for cost-effective software deployment and provides practical guidelines for writing code that can be moved between environments with minimal rework. The lecture covers common portability pitfalls and how to avoid them.
🗂️ Topics Covered
The lecture explores portability through multiple dimensions: sticking to standards and programming in the mainstream, data type size variations across platforms, order of evaluation differences, signedness of char, arithmetic vs logical shift operators, byte order and data exchange issues, alignment within structures, and the problems with bit fields. Each topic is illustrated with code examples showing how platform-dependent behavior can cause failures.
📝 Lecture Summary
10.13 Portability
Many applications need to be ported onto many different platforms. As we have seen, it is pretty hard to write error free, efficient, and maintainable software. So, if a major rework is required to port a program written for one environment to another, it will probably not come at a low cost. We ought to find ways and means by which we can port applications to other platforms with minimum effort. The key lies in how we write our program. If we are careful during writing code, we can make it portable. On the other hand if we write code without portability in mind, we may end up with code that is extremely hard to port to other environments.
Stick to the standard
- Use ANSI/ISO standard C++
- Instead of using vendor specific language extensions, use STL as much as possible
Program in the mainstream
Although C++ standard does not require function prototypes, one should always write them.
double sqrt(); // old style acceptable by ANSI C
double sqrt(double); // ANSI – the right approach
Size of data types
Sizes of data types cause major portability issues as they vary from one machine to the other.
int i, j, k;
...
j = 20000;
k = 30000;
i = j + k;
// works if int is 4 bytes
// what will happen if int is 2 bytes?
Order of Evaluation
As mentioned earlier during the discussion of side effects, order of evaluation varies from one implementation to another. This therefore also causes portability issues.
Signedness of char
The language does not specify whether char is signed or unsigned.
char c;
// between 0 and 255 if unsigned
// -128 to 127 if signed
c = getchar();
if (c == EOF) ??
// will fail if it is unsigned
It should therefore be written as follows:
int c;
c = getchar();
if (c == EOF)
🔑 Definition — EOF: End-of-file indicator, a special value (typically -1) returned by functions like getchar() to indicate that no more data is available.
📌 Example: Using char c = getchar() and checking if (c == EOF) will fail if char is unsigned (0-255 range) because EOF is typically -1, which cannot be represented in unsigned char.
Arithmetic or Logical Shift
The C/C++ language has not specified whether right shift >> is arithmetic or logical. In arithmetic shift the sign bit is copied while logical shift fills the vacated bits with 0. This obviously reduces portability.
Interestingly, Java has introduced a new operator to handle this issue. >> is used for arithmetic shift and >>> for logical shift.
🔑 Definition — Arithmetic shift: A bitwise shift operation that preserves the sign bit by copying it into the vacated positions on right shift. 🔑 Definition — Logical shift: A bitwise shift operation that fills vacated positions with 0 regardless of the sign.
Byte Order and Data Exchange
The order in which bytes of one word are stored is hardware dependent. In Intel architecture the lowest byte is the most significant byte while in Motorola architecture the highest byte of a word is the most significant one. This causes problems when dealing with binary data. One should therefore only use text for data exchange. One should also be aware of internationalization issues and hence should not assume ASCII as well as English.
🔑 Definition — Byte order (endianness): The order in which bytes within a multi-byte word are stored in memory, either little-endian (least significant byte first) or big-endian (most significant byte first).
💡 Why this matters: When exchanging binary data between Intel and Motorola systems, the bytes will be interpreted in reverse order unless special care is taken.
Alignment
The C/C++ language does not define the alignment of items within structures, classes, and unions. Data may be aligned on word or byte boundaries.
struct X {
char c;
int i;
};
Address of i could be 2, 4, or 8 from the beginning of the structure. Therefore, using pointers and then typecasting them to access individual components will cause all sorts of problems.
Bit Fields
Bit fields allow the packing of data in a structure. This is especially useful when memory or data storage is at a premium. Typical examples:
- Packing several objects into a machine word (e.g., 1-bit flags compacted — Symbol tables in compilers)
- Reading external file formats (non-standard file formats could be read in, e.g., 9-bit integers)
C lets us do this in a structure definition by putting :bit length after the variable:
struct packed_struct {
unsigned int f1:1;
unsigned int f2:1;
unsigned int f3:1;
unsigned int f4:1;
unsigned int type:4;
unsigned int funny_int:9;
} pack;
Here the packed_struct contains 6 members: Four 1-bit flags f1..f3, a 4-bit type and a 9-bit funny_int.
C automatically packs the above bit fields as compactly as possible, provided that the maximum length of the field is less than or equal to the integer word length of the computer. If this is not the case, some compilers may allow memory overlap for the fields whilst others would store the next field in the next word.
Bit fields are a convenient way to express many difficult operations. However, bit fields do suffer from a lack of portability between platforms:
- Integers may be signed or unsigned
- Many compilers limit the maximum number of bits in the bit field to the size of an integer (16-bit or 32-bit varieties)
- Some bit field members are stored left to right, others are stored right to left in memory
- If bit fields are too large, the next bit field may be stored consecutively in memory (overlapping the boundary between memory locations) or in the next word of memory
Bit fields therefore should not be used.
⭐ Key Takeaways
The most critical things a student MUST remember from this lecture for the exam are: portability requires intentional design from the start, specifically by sticking to ANSI/ISO standards and using STL rather than vendor extensions. Data type sizes (especially int varying from 2 to 4 bytes), signedness of char, and the unspecified nature of right shift operations are major sources of portability bugs. Binary data exchange between different architectures fails due to byte order differences, so text should always be used instead. Structure alignment is implementation-defined and cannot be relied upon. Finally, bit fields are highly non-portable due to compiler-dependent storage order, signedness, and size limits, and should be avoided entirely.
🧠 Quick Revision Questions
- Why should you use
int cinstead ofchar cwhen callinggetchar()and checking for EOF? - What is the difference between arithmetic and logical right shift, and why does this matter for portability?
- When exchanging data between an Intel-based system and a Motorola-based system, what problem can occur with binary data and how should it be avoided?
- Why can't you reliably determine the address of member
iinstruct X { char c; int i; }across different compilers? - List four reasons why bit fields should not be used in portable code.
📘 Lecture 35 — Exception Handling
📖 Overview: This lecture introduces exception handling as a technique that separates error-handling code from normal code and provides a consistent mechanism for handling errors. It explores how exceptions increase code complexity by introducing invisible execution paths, and discusses the challenges of writing exception-safe code that guarantees proper behavior when exceptions occur.
🗂️ Topics Covered
The lecture covers exception handling mechanisms (try/catch/throw), how exceptions can cross function boundaries, the impact of exceptions on code complexity (analyzing 23 execution paths in simple code), exception safety guarantees (basic, strong, no-throw), and practical techniques for achieving exception safety including the use of auto_ptr and handling multiple side-effects.
📝 Lecture Summary
Exception handling
Exception handling is a powerful technique that separates error-handling code from normal code. It also provides a consistent error handling mechanism. The greatest advantage of exception handling is its ability to handle asynchronous errors.
The idea is to raise some error flag every time something goes wrong. There is a system that is always on the lookout for this error flag. The raising of the imaginary error flag is simply called raising or throwing an error. When an error is thrown the overall system responds by catching the error. Surrounding a block of error-sensitive code with exception handling is called trying to execute a block.
One of the most powerful features of exception handling is that an error can be thrown over function boundaries. This allows programmers to put the error handling code in one place, such as the main-function of your program.
🔑 Definition — Exception handling: A technique that separates error-handling code from normal code and provides a consistent mechanism for handling asynchronous errors.
📐 Concept: try/catch/throw → try block contains error-sensitive code, throw raises an error, catch handles the thrown error
📌 Example:
try {
// error-sensitive code
throw Exception()
} catch( Exception e )
{
// error handling code
}
Exceptions and code complexity
A number of invisible execution paths can exist in simple code in a language that allows exceptions. The complexity of a program may increase significantly if there are exceptional paths in it.
Consider the following code:
String EvaluateSalaryAnadReturnName( Employee e)
{
if (e.Title() == "CEO" || e.Salary() > 10000)
{
cout << e.First() << " " << e.Last() << " is overpaid" << endl;
}
return e.First() + " " + e.Last();
}
Assumptions:
- Different order of evaluating function parameters are ignored
- Failed destructors are ignored
- Called functions are considered atomic
- To count as different execution paths, an execution path must be made-up of a unique sequence of function calls performed and exited in the same way
Question: How many more execution paths are there? Ans: 23 — There are 3 non-exceptional paths and 20 exceptional paths.
Non-exceptional paths:
- If
e.Title() == "CEO"is true, second part not evaluated, cout performed - If
e.Title() != "CEO"ande.Salary() > 10000, both parts evaluated, cout performed - If
e.Title() != "CEO"ande.Salary() <= 10000, both parts evaluated, cout not performed
Exceptional Code Paths (20 paths):
- Copy constructor for passing
eby value might throw 2-3.e.Title()might throw or its return copy might throw 4-5. String literal conversion to temporary object for==might throw 6-8.operator==()might throw 9-13. Any of the five<<operator calls might throw 14-15. Similar to 2 and 3 fore.First()ande.Last()16-19. Similar to 14-15 and 6-8 for return value construction - Similar to 4 for return value
🔑 Definition — Invisible execution paths: Additional execution paths created by exceptions that are not visible in the normal flow of code
💡 Why this matters: Simple looking code can have many hidden exceptional paths (23 vs 3 normal paths) — always be exception-aware.
Exception-Safety and Exception Neutrality
Exception-Safety: A function is exception safe if it might throw but does not have any side effects if it does throw, and any objects being used, including temporaries, are exception safe and clean-up their resources when destroyed.
Exception Neutral: A function is said to be exception neutral if it propagates all exceptions to the caller.
Levels of Exception Safety
- Basic Guarantee: Ensures that temporaries are destroyed properly and there are no memory leaks
- Strong Guarantee: Ensures basic guarantee as well as there is full-commit or roll-back
- No-throw Guarantee: Ensures that a function will not throw
Analysis of the original function:
- Basic Guarantee? Yes — the function does not create any objects, so no resource leaks
- Strong Guarantee? No — two distinct side-effects exist: emitting message to cout and returning a name string. If exception occurs after partial message emission or after full message but before return, state is inconsistent
- No-throw Guarantee? No — many operations might throw
Strong Guarantee Attempts
First attempt:
String EvaluateSalaryAnadReturnName( Employee e)
{
String result = e.First() + " " + e.Last();
if (e.Title() == "CEO" || e.Salary() > 10000)
{
String message = result + " is overpaid\n";
cout << message;
}
return result;
}
Problem: When caller assigns theName = evaluateSalarayAndReturnName(someEmployee), if copy constructor or copy assignment fails, the side-effects are completed but the result is irretrievably lost.
Second attempt:
String EvaluateSalaryAnadReturnName( Employee e, String &r)
{
String result = e.First() + " " + e.Last();
if (e.Title() == "CEO" || e.Salary() > 10000)
{
String message = result + " is overpaid\n";
cout << message;
}
r = result;
}
Problem: Assignment to r might still fail, leaving one side-effect completed and other incomplete.
Third attempt (using auto_ptr):
auto_ptr<String> EvaluateSalaryAnadReturnName( Employee e)
{
auto_ptr<String> result = new String(e.First() + " " + e.Last());
if (e.Title() == "CEO" || e.Salary() > 10000)
{
String message = (*result) + " is overpaid\n";
cout << message;
}
return result; // rely on transfer of ownership (can't throw)
}
Solution: Hides all work to construct the second side-effect (the return value) and ensures it can be safely returned using only non-throwing operation after the first side-effect completes. The auto_ptr semantics guarantee that if the caller accepts the returned value, they take ownership; if they ignore it, the allocated string is automatically destroyed with proper clean-up.
Exception Safety and Multiple Side-effects
It is difficult and sometimes impossible to provide strong exception safety when there are two or more side-effects in one function and these side-effects are not related with each other. For example, two output messages (one to cout, one to cerr) cannot be combined.
When such a situation occurs with two or more unrelated side-effects which cannot be combined, the best way to handle it is to break it into two separate functions. That way, the caller would know that these are two separate atomic steps.
⭐ Key Takeaways
Exception handling allows errors to cross function boundaries but significantly increases code complexity — a simple 3-line function can have 23 execution paths (3 normal + 20 exceptional). There are three levels of exception safety: basic (no leaks), strong (commit/roll-back), and no-throw (never throws). Achieving strong exception safety often requires performance trade-offs, like using auto_ptr to manage return value ownership. When a function has multiple unrelated side-effects that cannot be combined, it may be impossible to provide strong exception safety, and splitting into separate functions is the best approach.
🧠 Quick Revision Questions
- What is the difference between exception safety and exception neutrality?
- How many total execution paths exist in the
EvaluateSalaryAnadReturnNamefunction and why? - What are the three levels of exception safety guarantees?
- Why does the original
EvaluateSalaryAnadReturnNamefunction fail to meet the strong guarantee? - How does using
auto_ptrhelp achieve strong exception safety in the third attempt?
📘 Lecture 36 — Software Verification and Validation
📖 Overview: This lecture introduces the foundational concepts of software testing, including verification and validation, defect analysis, and the limitations of testing. It explains why testing cannot prove the absence of defects and establishes the importance of systematic test case design, setting the stage for black-box and white-box testing techniques covered in subsequent lectures.
🗂️ Topics Covered
The lecture covers the core definitions of verification versus validation, the nature of software defects, and the fundamental objectives of software testing. It then explores the limitations of testing through a detailed code example, the structure of test cases and test data, the parallel lifecycle of testing and development, and the distinct roles of developers and testers. Finally, it introduces black-box and structural (white-box) testing as two fundamental approaches.
📝 Lecture Summary
11.1 Software Testing
To understand software testing correctly, we must first understand related concepts. Software verification and validation are processes that check a product against its specifications and user expectations. According to Barry Boehm, Verification asks: "Does the product meet system specifications?" and "Have you built the product right?" In contrast, Validation asks: "Does the product meet user expectations?" and "Have you built the right product?" It is possible for software to fulfill its specifications but deviate from user expectations, meaning it is verified but not validated. This can happen when user needs were not captured precisely during requirements engineering.
11.2 Defect
A defect is a variance from a desired product attribute, involving system specifications and user expectations. Anything that causes customer dissatisfaction is a defect. A software defect is a phenomenon where software deviates from its expected behavior, representing non-compliance with written specifications or stakeholder needs. As Kernighan stated: "Death, taxes, and bugs are the only certainties in the life of a programmer." Software and defects cannot be separated, but discovering defects at an appropriate stage improves software quality.
🔑 Definition — Software Testing: The process of examining a software product against its requirements, involving verification of the product against written requirements and conformance of requirements with user needs. It is also the process of executing software on test data and examining its output vis-à-vis documented behavior.
Software testing objective: According to Popper (1965), the correct approach is not to verify a theory but to seek to refute it. The goal of testing is to expose latent defects before the software is put to use. A tester tries to break the system to show the presence of a defect, not its absence. Testing cannot show the absence of a defect; it only increases confidence because exhaustive testing is impossible — it requires virtually infinite resources.
🔑 Definition — Successful Test: As Myers (1979) stated, if your task is to find problems, you will look harder than if you think your task is to verify the program has none. A test is successful if it discovers an error, analogous to a doctor's diagnosis. Success depends on the ability to discover a bug, not prove the absence of one.
📌 Example (adapted from Backhouse): A function compares two strings for equality. A tester devised test cases including "cat"/"dog" (False), ""/"" (True), "hen"/"hen" (True), "hen"/"heN" (False), " "/"" (False), ""/"ball" (False), "cat"/"" (False), "HEN"/"hen" (False), "rat"/"door" (False), and " "/" " (True). The function passed all tests but still had a defect. The code bool isStringsEqual(char a[], char b[]) used strlen comparison and a loop checking a[i] == b[i] but set result = true on each matching character without resetting for mismatches. This caused "cut" and "rat" to return true, which is incorrect.
Testing limitations: To prove a formula is incorrect requires only one counterexample, but millions of examples cannot prove it correct — they only enhance comfort level. You cannot test a program completely because the domain of possible inputs is too large and there are too many possible paths through the program.
11.3 Test Cases and Test Data
Test cases correspond to application functionality and include input/output specifications, a statement of the function under test, steps to perform the function, and expected results. Test data includes inputs devised to test the system.
🔑 Definition — Test Case: A set of steps that should be followed to achieve certain functionality, including input/output specification plus steps and expected results.
11.3 Testing vs. Development
Testing is an intellectually demanding activity with a lifecycle parallel to software development. It demands grip over domain and application functionality, scenario-building capabilities, and destructive instincts to break the system. The functional specification document (FS) is the starting point for both testing and development. The development team performs analysis, design, and coding while the testing team performs analysis for test planning, test cases, and test data generation. System comes into testing after development. Test cases are executed, actual results are compared with expected results, and upon discovering defects, the tester generates a bug report. The development team reproduces the defect, identifies root cause, fixes it, and sends a patch. The testing team verifies the fix.
11.5 The Developer and Tester
Development is a creative activity with the objective to show the program works. Testing is a destructive activity with the objective to show the program does not work. Scenarios missed during development analysis would never be tested correctly because corresponding test cases would be missing or incorrect. If the same person who developed a system tests it, chances of carrying the same misunderstanding into testing are very high. Therefore, an independent testing team is essential.
11.6 Usefulness of Testing
The objective of testing is to discover and fix as many errors as possible before software is shipped to the client. Without testing, clients may demand free fixes or sue for damages. Testers are essential — a good tester has a knack of smelling errors, like auditors.
11.7 Testing and Software Phases
Testing phases include: Unit testing (individual components), Module testing (collection of dependent components), Subsystem testing (collection of modules to find interfacing problems), System testing (system as a whole), Acceptance test (validation against user expectations, usually at client premises), Alpha testing (acceptance testing for customized projects, in-house for products), and Beta testing (field testing with potential customers before general release).
11.8 Black Box Testing
In black box testing, a component or system is treated as a black box and tested for required behavior without concern for how inputs are transformed into outputs. The tester gives inputs through the system's interface and tests the output against expected results.
11.9 Structural Testing (White Box)
In structural or white box testing, we look inside the system and evaluate what it consists of and how it is implemented. We analyze internal structures like design, code structure, and documentation to devise test cases.
Effective testing: The objective is to discover the maximum number of defects with minimum resources before delivery. A good tester carries out thorough analysis to develop a representative set of test cases from a huge set of possibilities.
🔑 Definition — Equivalence Classes: Two tests are considered equivalent if it is believed that if one discovers a defect, the other probably will too, and if one does not discover a defect, the other probably won't either.
Equivalence partitioning guidelines: Organize classes in some order, determine boundary conditions, and do not forget invalid inputs.
📌 Example: For string matching, equivalence partitions include: Equal strings: Two equal strings of arbitrary length (lower case "cat"/"cat", upper case "CAT"/"CAT", mixed case "Cat"/"Cat", numeric "123"/"123", strings with blanks only " "/" ", numeric and character mixed "Cat1"/"Cat1", strings with special characters "Cat#1"/"Cat#1"), and two NULL strings ""/"". Unequal strings: Two different equal strings of arbitrary length (different length "cat"/"mouse", same length "cat"/"dog"), case sensitivity check ("Cat"/"caT"), and one string empty (first NULL ""/"cat", second NULL "cat"/"").
11.10 Basis Code Structures
There are four basic coding structures: sequence, if statement, case statement, and while loop. Flow graph notation describes flow of data or control. Sequence lumps several sequential instructions in one node. If has a decision node with two branches (true and false). Case has a switch node with multiple branches. While has a loop guard that controls iteration.
📌 Example: Flow graph for bubble sort has six nodes. Node 1 is the while loop, node 2 is the for loop, node 3 is swapping, nodes 4-6 are ending instructions. Possible paths include: Path1: 1-6, Path2: 1-2-3-4-5-1-6, Path3: 1-2-4-5-1-6, Path4: 1-2-4-2-3-4-5-6-1.
White Box Testing
Three coverage schemes are used: Statement Coverage tests all statements on a path; Branch Coverage tests all possible branches of decision structures; Path Coverage tests all possible paths from input to output.
📌 Example for sorted = false; while (!sorted) { sorted = true; for (i=0; i < N-1; i++) { if a[i] > a[i+1] { swap(a[i], a[i+1]); sorted = false; } } }:
- Statement coverage: execute with any valid input
- Branch coverage: if condition equals branches 1-2 and 1-3
- Path coverage: same as branch testing for simple if statements
📌 Example: For a loop for (i = 0; i < N; i++) { if (condition1) // do something else // do something else }:
- N=0: 1 path (1-5)
- N=1: 2 paths (1-2-4-1-5, 1-3-4-1-5)
- N=2: 4 paths
- Generalizing: 2^N paths possible. For N=20, more than 1 million paths.
Cyclomatic complexity is a quantitative measure of logical complexity that defines the number of independent paths in a program's basis set. It provides an upper bound for the number of tests that must be conducted to ensure all statements and branches are executed at least once.
📐 Formula: V(G) = E - N + 2, where E is the number of edges and N is the number of nodes in the flow graph G.
📌 Example: For the bubble sort flow graph with 8 edges and 6 nodes: C(G) = 8 - 6 + 2 = 4. Paths to test: Path1: 1-6, Path2: 1-2-3-4-5-1-6, Path3: 1-2-4-5-1-6, Path4: 1-2-4-2-3-4-5-6-1.
🔑 Definition — Infeasible Path: A path through a program that is never traversed for any input data. A good programming practice minimizes infeasible paths to zero.
📌 Example: Code with if (a == b) c = c-1; if (a != b) c = c+1; has infeasible paths 1-2-3-4-5 and 1-3-5. Modified code with if (a == b) c = c-1; else c = c+1; has no infeasible paths.
⭐ Key Takeaways
The most critical concept is that verification ("built the product right") and validation ("built the right product") are distinct but equally important processes — software can meet specifications yet fail user expectations. Testing is a destructive activity aimed at discovering defects, not proving their absence, and a successful test is one that finds a bug. Exhaustive testing is impossible due to infinite input domains and program paths, so equivalence partitioning helps select representative test cases by dividing the input domain into classes where tests are considered equivalent. Cyclomatic complexity (V(G) = E - N + 2) provides a practical upper bound for the number of test cases needed to achieve statement and branch coverage, while minimizing infeasible paths through proper coding practices improves testability.
🧠 Quick Revision Questions
- What is the fundamental difference between verification and validation according to Barry Boehm?
- Why can't testing prove the absence of defects, and what does a "successful test" mean?
- How did the string equality example demonstrate a limitation of testing despite passing all test cases?
- What are equivalence classes, and why is it important to include invalid inputs and boundary conditions when partitioning?
- What does cyclomatic complexity measure, and how does it help determine the number of test cases needed for white-box testing?
📘 Lecture 40 — Unit Testing
📖 Overview: This lecture introduces unit testing as a fundamental software testing technique where individual program units are tested in isolation. It covers the principles, benefits, and best practices of unit testing, then extends into defect removal efficiency, defect origination, and the comparison between inspection and testing techniques, emphasizing that testing alone is insufficient for high-quality software.
🗂️ Topics Covered
Unit testing principles and benefits, testing against the contract with a square root example, unit testing tips for project organization, defect removal efficiency showing how combining inspections with testing dramatically improves defect detection rates (from 53% to 99.9%), defect origination points throughout the development lifecycle, the complementary nature of inspection versus testing, inspection pre-conditions and checklists covering various fault classes, and static analyzers as supplementary tools for code verification.
📝 Lecture Summary
11.11 Unit testing
A software program is made up of units that include procedures, functions, classes, etc. The unit testing process involves the developer in testing of these units in isolation to verify their behavior. Typically, the unit test establishes an artificial environment, invokes routines in the module being tested, then checks results against known values or previous test runs (regression testing). When modules are assembled, the same tests can test the system as a whole.
Software should be tested more like hardware with built-in self testing (each unit tested independently), internal diagnostics (diagnostics defined for program units), and a test harness. The emphasis is on built-in testability from the very beginning.
Unit Testing Principles:
- Developers test their own code units during implementation.
- Normal and boundary inputs against expected results are tested.
- Unit testing is a great way to test an API.
Quantitative Benefits:
- Repeatable: Unit test cases can be repeated to verify no unintended side effects from code modifications.
- Bounded: Narrow focus simplifies finding and fixing defects.
- Cheaper: Find and fix defects early.
Qualitative Benefits:
- Assessment-oriented: Writing unit tests forces dealing with design issues like cohesion and coupling.
- Confidence-building: Know what works at an early stage; easier to change when retesting is easy.
Testing against the contract (Example) When writing unit tests, we write test cases that ensure a given unit honors its contract. This reveals whether the code meets the contract and whether the contract means what we think.
Contract for square root routine:
result = squareRoot(argument);
assert (abs (result * result – argument) < epsilon);
🔑 Definition — Unit Testing: Testing individual program units (procedures, functions, classes) in isolation to verify their behavior against expected results.
📌 Example — Square Root Contract Testing: The contract tells us what to test:
- Pass in a negative argument and ensure it is rejected.
- Pass in an argument of zero to ensure it is accepted (boundary value).
- Pass in values between zero and the maximum expressible argument and verify that the difference between the square of the result and the original argument is less than some value epsilon.
When designing a module, you should design both its contract and the code to test that contract. By building tests before implementing code, you try out the interface before committing to it.
Unit Testing Tips:
- For small projects, embed unit tests in the module itself.
- For larger projects, keep tests in the package directory or a /test subdirectory.
- Making code accessible provides developers with examples of how to use all functionality and a means to build regression tests.
- Use the main routine with conditional compilation to run unit tests.
11.12 Defect removal efficiency
Defect removal efficiency is the ability to remove defects from an application. Data published after analyzing 1500 projects, where four quality assurance mechanisms were employed, shows:
- Testing alone removes only 53% of defects (worst) to 60% (best), median 53%.
- Testing + Quality Assurance yields up to 65% efficiency.
- Code Inspection + Testing yields up to 75%.
- Design Inspections + Testing yields up to 80%.
- Design Inspections + Quality Assurance + Testing yields up to 95%.
- All four techniques combined yields up to 99.9%.
| Technique Combination | Worst | Median | Best |
|---|---|---|---|
| Testing alone | 30% | 53% | 60% |
| Testing + QA | 50% | 65% | 75% |
| Code Inspection + Testing | 55% | 70% | 80% |
| Design Inspection + Testing | 65% | 80% | 87% |
| Design Insp. + QA + Testing | 75% | 87% | 93% |
| All four techniques | 77% | 97% | 99.9% |
🔑 Definition — Defect Removal Efficiency: The measure of how effectively defects are removed from an application using various quality assurance mechanisms.
Inspection and chaotic zone A chaotic zone forms when defects are not discovered and fixed at the appropriate stage. These defects pile up at testing and maintenance phases, destabilizing the application. Fixing requirement or design defects during testing or maintenance becomes extremely expensive as underlying code must be changed.
💡 Why this matters: Testing alone does not suffice. Inspection techniques must be combined with testing to increase defect removal effectiveness.
11.13 Defect origination
In inspections, emphasis is on early detection and fixing of defects. Points in the development lifecycle where defects enter include:
- Requirements
- Design
- Coding
- User documentation
- Testing itself (can cause defects due to bad fixes)
- Change requests at maintenance or initial usage time
It is important to identify defects and fix them as near to their point of origination as possible.
🔑 Definition — Defect Origination: The stage in the development lifecycle where a defect is introduced into the program.
Lecture No. 41 — 11.14 Inspection versus Testing
Inspections and testing are complementary, not opposing, verification techniques. Both should be used in the verification and validation process.
Key Differences:
- Inspections can check conformance with a specification but not conformance with the customer's real requirements.
- Inspections cannot check non-functional characteristics like performance, usability.
- Inspections do not require program execution and may be used before implementation.
- Many different defects may be discovered in a single inspection.
- In testing, one defect may mask another, so several executions are required.
- For inspections, checklists are prepared containing defect information. Reuse domain and programming knowledge helps prepare these checklists.
- Inspections involve people examining source representation to discover anomalies and defects.
- Inspections may be applied to any representation of the system (requirements, design, test data, etc.).
Inspection pre-conditions:
- A precise specification must be available.
- Team members must be familiar with organization standards.
- Syntactically correct code must be available.
- Inspectors should prepare a checklist to help during inspection.
Inspection checklists: Checklists of common errors should be developed and used to drive the inspection process. These are programming language dependent. For example, in a language with weak type checking, the checklist can be larger. Examples of language-dependent defects include variable initialization, constant naming, loop termination, and array bounds.
Inspection Checklist Example:
| Fault Class | Inspection Check |
|---|---|
| Exception management faults | Have all possible error conditions been taken into account? |
| Data faults | Are all program variables initialized before their values are used? Have all constants been named? Should the lower bound of arrays be 0, 1, or something else? Should the upper bound of arrays be size or size-1? If character strings are used, is a delimiter explicitly assigned? |
| Control faults | For each conditional statement, is the condition correct? Is each loop certain to terminate? Are compound statements correctly bracketed? In case statements, are all possible cases accounted for? |
| Input/Output faults | Are all input variables used? Are all output variables assigned a value before they are output? |
| Interface faults | Do all function and procedure calls have correct number of parameters? Do formal and actual parameter types match? Are the parameters in right order? If components access shared memory, do they have the same model of shared memory structure? |
| Storage management faults | If a linked structure is modified, have all links been correctly assigned? If dynamic storage is used, has space been allocated correctly? Is space explicitly de-allocated after it is no longer required? |
These inspection checks are outcomes of experience gained from developing or testing similar programs.
11.15 Static analyzers
Static analyzers are software tools for source text processing. They parse the program text and try to discover potentially erroneous conditions, bringing them to the attention of the verification and validation team. These tools are very effective as an aid to inspections but are a supplement to, not a replacement for, inspections.
Checklist for static analysis:
| Fault Class | Static Analysis Checks |
|---|---|
| Data faults | Variable used before initialization; variable declared but never used; variables assigned twice but never used between assignments; possible array bound violations; undeclared variables |
| Control faults | Unreachable code; unconditional branches into loops |
| Input/Output faults | Variable output twice with no intervening assignment |
| Storage Management fault | Unassigned pointers; pointer arithmetic |
🔑 Definition — Static Analyzers: Software tools that parse program text to discover potentially erroneous conditions without executing the program.
⭐ Key Takeaways
Unit testing is essential for verifying individual program units in isolation, and testing against a contract helps ensure code meets its specifications while revealing design issues early. Defect removal efficiency data proves that testing alone (53% median) is insufficient; combining design inspections, code inspections, and quality assurance with testing achieves up to 99.9% defect removal. Inspections and testing are complementary techniques — inspections catch specification and design issues early without program execution, while testing verifies actual runtime behavior including non-functional characteristics. Checklists for inspections and static analyzers provide systematic approaches to finding defects across multiple fault classes including data, control, interface, and storage management issues. The critical lesson is that defects must be detected and fixed as close to their point of origination as possible to avoid entering the chaotic zone where accumulated defects become extremely expensive to resolve.
🧠 Quick Revision Questions
- What are the three quantitative benefits of unit testing, and what does each mean?
- Using the square root contract example, what three test cases must be created to verify the contract?
- According to the defect removal efficiency data, what is the median defect removal rate for testing alone versus combining all four techniques?
- What is the chaotic zone in defect management, and why is it problematic?
- List at least four fault classes from the inspection checklist and provide one example inspection check for each.
📘 Lecture 42 — Debugging
📖 Overview: This lecture defines software bugs, traces the history of the term "debugging" from Admiral Grace Hopper's moth incident, and explains the critical importance of debugging in software maintenance. It then details the infamous 1990 AT&T outage caused by a simple code error, discusses debugging as both an art and a science, and introduces several major classes of bugs with examples, symptoms, and code snippets.
🗂️ Topics Covered
The lecture begins by defining what a bug is and provides a brief history of debugging from Grace Hopper's moth to modern debuggers. It explains the huge cost of debugging in the maintenance phase and presents a real-world case study of the 1990 AT&T telephone outage caused by a misplaced break statement. The lecture then distinguishes debugging as a scientific process rather than an art, using the "you miss the obvious" phenomenon to explain the need for another pair of eyes. Finally, it introduces major bug classes including memory/resource leaks, logical errors, coding errors, memory over-runs, loop errors, pointer errors, and Boolean bugs.
📝 Lecture Summary
12.1 Debugging
Nothing in software development is certain except death, taxes, and software bugs. A bug is defined as anything the software does that it is not supposed to do, or alternatively, something the software doesn't do that it is supposed to. Bugs range from program crashes to returning incorrect information to having garbled displays.
🔑 Definition — Bug: A software defect where the software does something it is not supposed to do, or does not do something it is supposed to.
12.2 A Brief History of Debugging
The term "bug" was coined by Admiral Grace Hopper while working on the Mark II computer at Harvard University. The first actual "bug" was a moth that flew into a relay, shorting out two contacts and shutting down the system. The moth was removed and pasted into the project logbook. From that point on, her team called their troubleshooting efforts "debugging."
Early debugging efforts centered around data dumps or output devices like printers and display lights. The next major evolution came with command-line debuggers, which represented the first real attempt to turn debugging from a hit-or-miss proposition into a reproducible process.
💡 Why this matters: Understanding the history shows that debugging has evolved from a haphazard activity into a structured process that is essential for modern software development.
Importance of Debugging
During the maintenance phase, 20% of the lifecycle cost is attributed to defects found after installation. Since maintenance accounts for 2/3rd of the overall software cost, this 20% represents a huge financial burden. System downtime puts tremendous pressure on developers, and every second of outage costs huge losses to the organization.
12.4 Problem at AT&T
On January 15, 1990, AT&T had a US-wide telephone system outage lasting nine hours. The cause was a program error in software meant to make the system more efficient. The code snippet that caused the outage is illustrated below:
do {
switch (expression) {
case 0: {
if (some_condition) {
// ...
break; // Line 7 - the culprit
} else {
// ...
}
// ...
break;
}
}
} while (some_other_condition);
🔑 Definition — The Culprit: The break statement at line 7 caused the program to exit the entire switch block, when the programmer intended it to only break the if-then clause and continue execution at line 11.
📐 Lesson: Code inspection might have caught this bug only if another engineer saw this line of code and questioned the original programmer's intention.
12.5 Art and Science of Debugging
Debugging is taken as an art but in fact it is a scientific process. As people learn about different defect types, they develop heuristics — shortcuts that help solve similar problems faster next time.
The lecture introduces the phenomenon of "you miss the obvious." When a person writes code, they develop a personal bias toward their creation. When checking this code, they can potentially miss obvious mistakes due to this bias. Therefore, it is strongly recommended to use "another pair of eyes" — ask a companion to help discover the defect.
Example — Bulletin Board Code
while (i = 0; i < 10; i++) {
cout << i << newl;
}
The problem is obvious once pointed out: the loop uses while syntax but the syntax inside is actually a for loop.
Lecture No. 43 — Bug Classes
12.6 Bug Classes
Memory and Resource Leak
A memory leak occurs when memory is allocated from either the operating system or an internal memory pool, but never deallocated when the memory is finished being used.
Symptoms: System slowdowns, crashes that occur "randomly" over a long period of time.
Example 1:
char *buffer = new char[kMaxBufferSize+1];
memset(buffer, 0, kMaxBufferSize+1);
if (IsError(nCondition)) {
return FailureCode; // Memory not freed here!
}
delete buffer;
return okCode;
If no error occurs, memory is freed properly. But if an error occurs, the memory is not freed — a leak occurs.
Example 2:
class Foo {
char *sString;
public:
Foo() { sString = new char[21]; }
~Foo() { delete sString; }
void SetString(const char *inString) {
sString = new char[strlen(inString+1)]; // Old memory lost
if(inString == NULL) return;
strncpy(sString, inString, strlen(inString));
}
};
🔑 Issue: The SetString method overwrites the previously allocated string without freeing it, causing an instant memory leak.
Logical Errors
A logical error occurs when the code is syntactically correct but does not do what you expect it to do.
Symptoms: Code misbehaves in an unexplainable way, program flow takes odd branches, results are opposite of expected, output looks strange.
Example:
if((input >= 1 && input <= 10) && (input >= 15 && input <= 20)) {
// Valid case
} else {
// Invalid case
}
🔑 Problem: A number cannot simultaneously be between 1-10 AND between 15-20. This is a typical logical error using && when || was intended.
📐 Formula for logical errors: When validating ranges that have gaps, always use logical OR (||) for disjoint ranges, not AND (&&).
Coding Errors
A coding error is a simple problem in writing the code — failure to check error returns, failure to check for valid conditions, or failure to account for other parts of the system.
Symptoms: Unexpected errors in black box testing, compiler warnings, lack of attention to details.
Example:
void convertToString(int InInteger, char* OutString, int* OutLength) {
switch(InInteger) {
case 1: OutString = "One"; OutLength = 3; break;
case 2: OutString = "Two"; OutLength = 3; break;
// ... cases 3-9
}
// No default case — zero value crashes
}
🔑 Issues: Does not handle all cases (e.g., zero value), does not initialize output variables.
Memory Over-runs
A memory overrun occurs when you use memory that does not belong to you — overstepping an array boundary or copying a string too big for its allocated block.
Symptoms: Program crashes regularly after a given routine, or a prior routine has already trashed variables.
Example:
const kMaxEntries = 50;
int gArray[kMaxEntries];
int ZeroArray(int *pArray) {
for(int i=0; i<100; ++i) // Writes past 50-element array
pArray[i] = 0;
}
🔑 Problem: The loop goes past the array's 50-element boundary, corrupting memory beyond its control.
Loop Errors
Loop errors break down into several subtypes: infinite loops, off-by-one loops, and improperly exited loops.
Symptoms: Program locks up (infinite loop), incorrect calculations by the last data point (off-by-one), process terminates unexpectedly.
Example 1 — Infinite Loop:
bool doneFlag = false;
while(!doneFlag) {
if(impossibleCondition)
doneFlag = true; // Never becomes true
}
Example 2 — Off-by-One:
int anArray[50];
int i = 50;
while(i >= 0) {
anArray[i] = 0; // Accesses index 50 — out of bounds
i = i - 1;
}
The loop performs 51 times but the array only has 50 elements.
Example 3 — Improper Exit Condition:
for(int i=0; i<kMaxIterations; ++i) {
while(nIndex < 20) {
ComputeSomething(i*20 + nIndex);
nIndex ++;
}
}
First iteration works correctly, but nIndex becomes 21 and the inner loop never executes for subsequent outer loop iterations.
Pointer Errors
A pointer error occurs when something is being used as an indirect pointer to another item — uninitialized pointers, deleted pointers still being used, or invalid pointers.
Symptoms: Program crashes or behaves unpredictably, stack corruptions, memory allocation failures, odd changing of variable values.
Example:
void cleanup_function(char *ptr) {
SaveToDisk(ptr);
delete ptr; // First delete
}
int func() {
char *s = new char[80];
cleanup_function(s);
delete s; // Second delete — double deletion!
}
🔑 Problem: The pointer is deleted twice — once in cleanup_function and once in func(). At best, the delete recognizes this; at worst, memory is corrupted.
Boolean Bugs
Boolean bugs occur because the mathematical precision of Boolean algebra has virtually nothing to do with equivalent English words. When we say "and," we really mean the boolean "or" and vice versa.
Symptoms: Program does exactly the opposite of what you expect.
Example:
int DoSomeAction(int InputNum) {
if(InputNum < 1 || InputNum > 10) return 0; // Failure
else {
PerformTheAction(InputNum);
return NumberOfAction + 1; // Returns nonzero for success
}
}
Ret = DoSomeAction(11); // Error case
if(Ret) // Ret is 0 (failure), but condition checks for nonzero
cout << "Error in DoSomeAction";
🔑 Problem: The function returns 0 for failure and nonzero for success. This is counter-intuitive — the error message is never triggered because the condition if(Ret) expects nonzero for failure.
Lecture No. 44 — Holistic Approach & Debugging Process
12.7 Holistic Approach
Holistic means emphasizing the importance of the whole and the interdependence of its parts — concerned with wholes rather than analysis into parts.
In debugging, a holistic approach focuses on the entire system rather than whatever piece appears broken. You cannot treat symptoms; you must focus on the application system as a whole.
Example: A program crashes on mX = X; in a simple setter function void SetX(int X) { mX = X; }. Inserting a message statement makes the problem "go away" — but the problem is not fixed. This is treating symptoms rather than finding the root cause.
12.8 The Debugging Process
In normal circumstances, you will have a user description of the problem. This data must be considered suspiciously until you can get a first-hand description.
Three user accounts of the same bug:
- "I selected Favorites from the menu, scrolled to third entry, pressed Enter, then clicked fourth entry on submenu, entered my name, clicked OK... crashed."
- "I selected New menu option, clicked Create Object, entered HouseObject, clicked OK... crashed."
- "I selected New Project menu option, clicked Gear icon, entered Rudolph, clicked OK... crashed."
Analysis process:
- Each user clicked the OK button to finalize the process → likely OK handler contains a fatal flaw
- Each user worked with a menu → examine menu handler for mouse vs keyboard issues
- Each user used both keyboard and mouse → look for combination handling bugs
Good Clues, Easy Bugs — Get a Stack Trace:
- Source line numbers in stack trace are the single most useful piece of debugging information
- Check argument values for improbability (zero, very large, negative, non-alphabetic character strings)
- Use debuggers to display local or global variables
Non-reproducible Bugs:
- Bugs that won't "stand still" (almost random) are the most difficult
- Randomness itself is information
- Check if all variables are initialized
- If bug disappears when debugging code is inserted, suspect memory allocation (
malloc) problems - Check for dangling pointers
Example — Dangling Pointer:
char *msg(int n, char *s) {
char buf[100];
sprintf(buf, "error %d: %s\n", n, s);
return buf; // Returns pointer to local array
}
p = msg(20, "Output values");
q = msg(30, "Input values");
printf("%s\n", p); // p points to invalid memory (stack frame destroyed)
🔑 Problem: Returns a pointer to a local array buf which is destroyed when the function returns.
⭐ Key Takeaways
Debugging is not merely an art but a scientific process requiring methodical investigation of code. The most critical concept is understanding that you will miss the obvious in your own code due to personal bias — always use "another pair of eyes." The AT&T 1990 outage teaches us that even a single misplaced break statement in a switch block can cause millions in losses. For the exam, memorize the seven bug classes (memory/resource leaks, logical errors, coding errors, memory over-runs, loop errors, pointer errors, and Boolean bugs) and their specific symptoms and examples. Finally, adopt a holistic approach — never treat symptoms alone; always investigate the entire system to find the root cause.
🧠 Quick Revision Questions
- What was the first actual "bug" in computing history, and who coined the term "debugging"?
- Describe the code error that caused the 1990 AT&T nine-hour outage. What was the programmer's intention versus what actually happened?
- List three symptoms of a memory leak bug and provide a code example where memory is not deallocated in all paths.
- What is the difference between a logical error and a coding error? Give one code example of each.
- What makes a "non-reproducible bug" particularly difficult to debug, and what two things should you check first when you encounter one?